sync_intune: rewrite as 5-phase status monitor with reboot detection
Replaces the 3-step pass/fail polling that lived in the .bat with a
PowerShell monitor that renders a full status table for the SFLD
enrollment lifecycle and handles the pre-reboot -> reboot -> post-reboot
transition explicitly.
Three structural problems with the old script:
1. Step 3 ("SFLD - Consume Credentials task exists") fired too early.
The task is created by SetupCredentials.log around 08:52 in the
pre-reboot phase, NOT post-reboot, so passing all 3 gates didn't
actually mean "fully done" - it just meant "credential setup ran".
2. No detection of the pre-reboot -> reboot -> post-reboot transition.
The script never read DSCDeployment.log, so it couldn't tell the
user "you need to reboot now to start the install phase". A device
stuck waiting for reboot was indistinguishable from one still
syncing.
3. No visibility into Phase 4 (per-script wrappers like Install-eDNC,
Install-UDC, Install-VCRedists, Install-OpenText). When something
hung you had to manually grep C:\Logs\SFLD\.
New layout:
sync_intune.bat - thin launcher (~50 lines): self-elevate, invoke
Monitor-IntuneProgress.ps1, branch on exit code
(0 = done / 2 = reboot needed / else = error).
Monitor-IntuneProgress.ps1 - the actual monitor (~340 lines):
- 5-phase status table (Identity / SFLD config / DSC deployment +
install / Custom scripts / Final) updated every 30s via Clear-
Host + redraw, with the QR code anchored at the top.
- Phase 4 auto-discovers custom scripts by parsing DSCInstall.log
for "Downloading script: <name>" lines AND scanning C:\Logs\SFLD\
Install-*.log files - so Display PCs running entirely different
scripts surface their own list automatically without hardcoding.
Statuses: pending / running / done / failed (mtime + tail-based).
- Boot-loop-safe reboot detection via Test-RebootState: only signals
'needed' if DSCDeployment.log was modified AFTER LastBootUpTime.
Once we've rebooted past it, just waits for DSCInstall.log.
- Caches monotonic Phase 1 indicators (AzureAdJoined, IntuneEnrolled,
EnterpriseMgmt task) so dsregcmd /status (slow ~1-2s) only runs
until the flag flips true, not on every poll.
- Triggers Intune sync at startup, re-triggers every 3 minutes (was
every 15 seconds in the old loop, which actively interrupted
in-flight CSP work).
Exit codes consumed by sync_intune.bat:
0 - DSCInstall.log shows "Installation completed successfully"
2 - DSCDeployment.log shows "Deployment completed successfully" AND
the deploy log is newer than LastBootUpTime (= reboot needed)
1 - error
Detection markers (decoded from a captured run at /home/camp/pxe-images/
Logs/ - see comment block at top of Monitor-IntuneProgress.ps1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
453
playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1
Normal file
453
playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1
Normal file
@@ -0,0 +1,453 @@
|
||||
# Monitor-IntuneProgress.ps1 - Real-time status display for the SFLD enrollment
|
||||
# lifecycle. Called from sync_intune.bat after admin elevation. Replaces the
|
||||
# 3-step pass/fail polling that lived in the .bat with a richer status table
|
||||
# spanning all 5 phases of the lifecycle.
|
||||
#
|
||||
# WHY THIS EXISTS:
|
||||
# The previous sync_intune.bat checked 3 things sequentially (SFLD reg key,
|
||||
# DSCInstall.log "Installation completed successfully", "SFLD - Consume
|
||||
# Credentials" task). Three problems with that:
|
||||
# 1. The Consume Credentials task is created PRE-reboot in
|
||||
# SetupCredentials.log, not POST-reboot, so it isn't a "fully done"
|
||||
# signal - it's a pre-reboot signal that overlaps with the SFLD reg key.
|
||||
# 2. There was no detection of the pre-reboot -> reboot -> post-reboot
|
||||
# transition. The script never read DSCDeployment.log, so it couldn't
|
||||
# tell the user "you need to reboot now".
|
||||
# 3. No visibility into Phase 4 (per-script wrappers like Install-eDNC,
|
||||
# Install-UDC, Install-VCRedists, Install-OpenText). When something hung
|
||||
# you had to manually grep C:\Logs\SFLD\.
|
||||
#
|
||||
# WHAT THIS SCRIPT DOES:
|
||||
# - Renders a 5-phase status table that updates every $PollSecs (default 30s)
|
||||
# - Triggers an Intune sync at startup, re-triggers every $RetriggerMinutes
|
||||
# (default 3 min) until the device hits the final state
|
||||
# - Auto-discovers Phase 4 custom scripts by parsing "Downloading script:"
|
||||
# lines from DSCInstall.log AND scanning C:\Logs\SFLD\Install-*.log files
|
||||
# - Boot-loop-safe reboot detection: only signals "reboot needed" if
|
||||
# DSCDeployment.log was modified AFTER the last system boot
|
||||
# - Caches dsregcmd output and other monotonic Phase 1 indicators (once
|
||||
# true, they don't go back to false)
|
||||
#
|
||||
# EXIT CODES (consumed by sync_intune.bat):
|
||||
# 0 = all done, post-reboot install complete, no further action needed
|
||||
# 2 = reboot required (deployment phase done, install phase pending)
|
||||
# 1 = error
|
||||
#
|
||||
# DETECTION REFERENCES (decoded from a real run captured at /home/camp/pxe-images/Logs/):
|
||||
# Phase A (pre-reboot, ~08:35-08:52):
|
||||
# - enrollment.log ppkg + computer name
|
||||
# - SetupCredentials.log creates HKLM\SOFTWARE\GE\SFLD\DSC + Consume Credentials task
|
||||
# - DSCDeployment.log ends "Deployment completed successfully"
|
||||
# writes C:\Logs\SFLD\version.txt
|
||||
# creates SFLD-ApplyDSCConfig scheduled task
|
||||
# [REBOOT]
|
||||
# Phase B (post-reboot, ~08:58):
|
||||
# - DSCInstall.log downloads device-config.yaml from blob,
|
||||
# processes Applications + CustomScripts,
|
||||
# ends "Installation completed successfully"
|
||||
# writes C:\Logs\SFLD\DSCVersion.txt
|
||||
# - C:\Logs\SFLD\Install-*.log one per CustomScript wrapper
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$PollSecs = 30,
|
||||
[int]$RetriggerMinutes = 3
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Helpers
|
||||
# ============================================================================
|
||||
|
||||
function Read-RegValue {
|
||||
param([string]$Path, [string]$Name)
|
||||
try { (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop).$Name }
|
||||
catch { $null }
|
||||
}
|
||||
|
||||
function Get-EnrollmentId {
|
||||
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -ErrorAction SilentlyContinue |
|
||||
Where-Object { (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).ProviderID -eq 'MS DM Server' } |
|
||||
Select-Object -First 1 -ExpandProperty PSChildName
|
||||
}
|
||||
|
||||
function Format-Age {
|
||||
param($Seconds)
|
||||
if ($null -eq $Seconds) { return "??" }
|
||||
$s = [double]$Seconds
|
||||
if ($s -lt 0) { return "now" }
|
||||
if ($s -lt 60) { return "{0:N0}s" -f $s }
|
||||
if ($s -lt 3600) { return "{0:N0}m {1:N0}s" -f [math]::Floor($s / 60), ($s % 60) }
|
||||
return "{0:N1}h" -f ($s / 3600)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Monotonic Phase 1 cache - dsregcmd is slow (~1-2s) and these flags don't
|
||||
# revert once true, so we only re-check until they pass.
|
||||
# ============================================================================
|
||||
$script:cache = @{
|
||||
AzureAdJoined = $false
|
||||
IntuneEnrolled = $false
|
||||
EmTaskExists = $false
|
||||
EnrollmentId = $null
|
||||
}
|
||||
|
||||
function Get-Phase1 {
|
||||
if (-not $script:cache.AzureAdJoined) {
|
||||
try {
|
||||
$dsreg = dsregcmd /status 2>&1
|
||||
if ($dsreg -match 'AzureAdJoined\s*:\s*YES') {
|
||||
$script:cache.AzureAdJoined = $true
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (-not $script:cache.IntuneEnrolled) {
|
||||
$eid = Get-EnrollmentId
|
||||
if ($eid) {
|
||||
$script:cache.EnrollmentId = $eid
|
||||
$script:cache.IntuneEnrolled = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $script:cache.EmTaskExists -and $script:cache.EnrollmentId) {
|
||||
try {
|
||||
$tp = "\Microsoft\Windows\EnterpriseMgmt\$($script:cache.EnrollmentId)\"
|
||||
$tasks = Get-ScheduledTask -TaskPath $tp -ErrorAction SilentlyContinue
|
||||
if ($tasks) { $script:cache.EmTaskExists = $true }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$policiesArriving = $false
|
||||
try {
|
||||
$children = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\PolicyManager\current\device' -ErrorAction SilentlyContinue
|
||||
$policiesArriving = (($children | Measure-Object).Count -gt 0)
|
||||
} catch {}
|
||||
|
||||
return @{
|
||||
AzureAdJoined = $script:cache.AzureAdJoined
|
||||
IntuneEnrolled = $script:cache.IntuneEnrolled
|
||||
EmTaskExists = $script:cache.EmTaskExists
|
||||
PoliciesArriving = $policiesArriving
|
||||
EnrollmentId = $script:cache.EnrollmentId
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 4: auto-discover custom scripts. Parse DSCInstall.log for
|
||||
# "Downloading script: <name>" lines to know what's EXPECTED, then look at
|
||||
# C:\Logs\SFLD\Install-*.log to know what's DONE/RUNNING/FAILED. Anything
|
||||
# expected but missing a log file is "pending".
|
||||
# ============================================================================
|
||||
function Get-CustomScriptStatuses {
|
||||
param([string]$DscInstallLog)
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) { return @() }
|
||||
|
||||
# Discover existing Install-*.log files
|
||||
$now = Get-Date
|
||||
$existing = @{}
|
||||
Get-ChildItem $logDir -Filter 'Install-*.log' -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$age = ($now - $_.LastWriteTime).TotalSeconds
|
||||
$tail = Get-Content $_.FullName -Tail 30 -ErrorAction SilentlyContinue
|
||||
$hasError = $false
|
||||
if ($tail) {
|
||||
$errMatch = $tail | Where-Object { $_ -match '(?i)\b(ERROR|Failed|exception)\b' } | Select-Object -First 1
|
||||
if ($errMatch) { $hasError = $true }
|
||||
}
|
||||
$status = if ($age -lt 30) { 'running' }
|
||||
elseif ($hasError) { 'failed' }
|
||||
else { 'done' }
|
||||
$existing[$_.BaseName] = @{
|
||||
Name = $_.BaseName
|
||||
Status = $status
|
||||
Age = $age
|
||||
LogFile = $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Parse DSCInstall.log for the expected script list
|
||||
$expected = @()
|
||||
if (Test-Path $DscInstallLog) {
|
||||
try {
|
||||
$hits = Select-String -Path $DscInstallLog -Pattern 'Downloading script:\s*(\S+)' -ErrorAction SilentlyContinue
|
||||
foreach ($h in $hits) {
|
||||
$name = $h.Matches[0].Groups[1].Value
|
||||
if ($name -notlike 'Install-*') { $name = "Install-$name" }
|
||||
if ($expected -notcontains $name) { $expected += $name }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# Build final ordered list: expected scripts first (in DSC order), then any
|
||||
# log files we found that weren't in the expected list (legacy / orphaned).
|
||||
$result = @()
|
||||
foreach ($name in $expected) {
|
||||
if ($existing.ContainsKey($name)) {
|
||||
$result += $existing[$name]
|
||||
} else {
|
||||
$result += @{ Name = $name; Status = 'pending'; Age = $null; LogFile = $null }
|
||||
}
|
||||
}
|
||||
foreach ($name in $existing.Keys) {
|
||||
if ($expected -notcontains $name) {
|
||||
$result += $existing[$name]
|
||||
}
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Boot-loop-safe reboot check.
|
||||
# Returns:
|
||||
# 'none' - install already complete OR pre-reboot phase not done
|
||||
# 'needed' - pre-reboot done AND we haven't rebooted since
|
||||
# 'in-progress' - pre-reboot done AND we already rebooted (just waiting for
|
||||
# post-reboot DSCInstall.log to finish)
|
||||
# ============================================================================
|
||||
function Test-RebootState {
|
||||
$deployLog = 'C:\Logs\SFLD\DSCDeployment.log'
|
||||
$installLog = 'C:\Logs\SFLD\DSCInstall.log'
|
||||
|
||||
if (Test-Path $installLog) {
|
||||
$tail = Get-Content $installLog -Tail 10 -ErrorAction SilentlyContinue
|
||||
if ($tail -match 'Installation completed successfully') { return 'none' }
|
||||
}
|
||||
|
||||
if (Test-Path $deployLog) {
|
||||
$tail = Get-Content $deployLog -Tail 10 -ErrorAction SilentlyContinue
|
||||
if ($tail -match 'Deployment completed successfully') {
|
||||
try {
|
||||
$lastBoot = (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).LastBootUpTime
|
||||
$deployTime = (Get-Item $deployLog -ErrorAction Stop).LastWriteTime
|
||||
if ($deployTime -gt $lastBoot) { return 'needed' }
|
||||
return 'in-progress'
|
||||
} catch {
|
||||
return 'needed'
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'none'
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Snapshot - one call collects everything we need to render
|
||||
# ============================================================================
|
||||
function Get-Snapshot {
|
||||
$p1 = Get-Phase1
|
||||
$function = Read-RegValue 'HKLM:\SOFTWARE\GE\SFLD\DSC' 'Function'
|
||||
$sasToken = Read-RegValue 'HKLM:\SOFTWARE\GE\SFLD\DSC' 'SasToken'
|
||||
|
||||
$deployLog = 'C:\Logs\SFLD\DSCDeployment.log'
|
||||
$installLog = 'C:\Logs\SFLD\DSCInstall.log'
|
||||
|
||||
$deployExists = Test-Path $deployLog
|
||||
$deployComplete = $false
|
||||
if ($deployExists) {
|
||||
$t = Get-Content $deployLog -Tail 10 -ErrorAction SilentlyContinue
|
||||
if ($t -match 'Deployment completed successfully') { $deployComplete = $true }
|
||||
}
|
||||
|
||||
$installExists = Test-Path $installLog
|
||||
$installComplete = $false
|
||||
if ($installExists) {
|
||||
$t = Get-Content $installLog -Tail 10 -ErrorAction SilentlyContinue
|
||||
if ($t -match 'Installation completed successfully') { $installComplete = $true }
|
||||
}
|
||||
|
||||
$consumeCredsTask = $false
|
||||
try {
|
||||
$task = Get-ScheduledTask -TaskName 'SFLD - Consume Credentials' -ErrorAction SilentlyContinue
|
||||
if ($task) { $consumeCredsTask = $true }
|
||||
} catch {}
|
||||
|
||||
$customScripts = Get-CustomScriptStatuses -DscInstallLog $installLog
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Function = $function
|
||||
Phase1 = $p1
|
||||
Phase2 = @{
|
||||
SfldRoot = (Test-Path 'HKLM:\SOFTWARE\GE\SFLD')
|
||||
FunctionOk = (-not [string]::IsNullOrWhiteSpace($function))
|
||||
SasTokenOk = (-not [string]::IsNullOrWhiteSpace($sasToken))
|
||||
}
|
||||
Phase3 = @{
|
||||
DeployLogExists = $deployExists
|
||||
DeployComplete = $deployComplete
|
||||
InstallLogExists = $installExists
|
||||
InstallComplete = $installComplete
|
||||
}
|
||||
Phase4 = $customScripts
|
||||
Phase5 = @{
|
||||
ConsumeCredsTask = $consumeCredsTask
|
||||
}
|
||||
DscInstallComplete = $installComplete
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Sync trigger (Schedule #3 task per Intune enrollment)
|
||||
# ============================================================================
|
||||
function Invoke-IntuneSync {
|
||||
try {
|
||||
$enrollPath = 'HKLM:\SOFTWARE\Microsoft\Enrollments'
|
||||
Get-ChildItem $enrollPath -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$provider = (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).ProviderID
|
||||
if ($provider -eq 'MS DM Server') {
|
||||
$id = $_.PSChildName
|
||||
$tp = "\Microsoft\Windows\EnterpriseMgmt\$id\"
|
||||
Get-ScheduledTask -TaskPath $tp -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.TaskName -match 'Schedule #3' } |
|
||||
ForEach-Object { Start-ScheduledTask -InputObject $_ -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# QR code (cached as text - generate once, re-print on every redraw)
|
||||
# ============================================================================
|
||||
function Build-QRCodeText {
|
||||
$lines = @()
|
||||
try {
|
||||
$dsreg = dsregcmd /status 2>&1
|
||||
$line = $dsreg | Select-String 'DeviceId' | Select-Object -First 1
|
||||
} catch {
|
||||
$line = $null
|
||||
}
|
||||
if (-not $line) {
|
||||
$lines += "Device not yet Azure AD joined."
|
||||
return ($lines -join "`n")
|
||||
}
|
||||
$deviceId = $line.ToString().Split(':')[1].Trim()
|
||||
$lines += "Intune Device ID: $deviceId"
|
||||
$lines += ""
|
||||
|
||||
$dllPath = 'C:\Enrollment\shopfloor-setup\Shopfloor\QRCoder.dll'
|
||||
if (Test-Path $dllPath) {
|
||||
try {
|
||||
Add-Type -Path $dllPath
|
||||
$gen = New-Object QRCoder.QRCodeGenerator
|
||||
$data = $gen.CreateQrCode($deviceId, [QRCoder.QRCodeGenerator+ECCLevel]::L)
|
||||
$ascii = New-Object QRCoder.AsciiQRCode($data)
|
||||
$qr = $ascii.GetGraphic(1, [char]0x2588 + [char]0x2588, ' ')
|
||||
$lines += $qr -split "`r?`n"
|
||||
} catch {
|
||||
$lines += "(QR code generation failed: $($_.Exception.Message))"
|
||||
}
|
||||
} else {
|
||||
$lines += "(QRCoder.dll not found at $dllPath - skipping QR code)"
|
||||
}
|
||||
return ($lines -join "`n")
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Renderer
|
||||
# ============================================================================
|
||||
function Format-Snapshot {
|
||||
param($Snap, $LastSync, $NextRetrigger)
|
||||
|
||||
function Mk { param([bool]$ok) if ($ok) { '[v]' } else { '[ ]' } }
|
||||
|
||||
$lines = @()
|
||||
$lines += "========================================"
|
||||
$lines += " Intune Lockdown Progress"
|
||||
if ($Snap.Function) {
|
||||
$lines += " Function: $($Snap.Function)"
|
||||
}
|
||||
$lines += "========================================"
|
||||
$lines += ""
|
||||
$lines += " Phase 1: Identity"
|
||||
$lines += " $(Mk $Snap.Phase1.AzureAdJoined) Azure AD joined"
|
||||
$lines += " $(Mk $Snap.Phase1.IntuneEnrolled) Intune enrolled"
|
||||
$lines += " $(Mk $Snap.Phase1.EmTaskExists) EnterpriseMgmt sync tasks"
|
||||
$lines += " $(Mk $Snap.Phase1.PoliciesArriving) Policies arriving"
|
||||
$lines += ""
|
||||
$lines += " Phase 2: SFLD configuration"
|
||||
$lines += " $(Mk $Snap.Phase2.SfldRoot) SFLD reg key"
|
||||
$lines += " $(Mk $Snap.Phase2.FunctionOk) Function set"
|
||||
$lines += " $(Mk $Snap.Phase2.SasTokenOk) DSC SAS token configured"
|
||||
$lines += ""
|
||||
$lines += " Phase 3: DSC deployment + install"
|
||||
$lines += " $(Mk $Snap.Phase3.DeployLogExists) DSCDeployment.log present"
|
||||
$lines += " $(Mk $Snap.Phase3.DeployComplete) Pre-reboot deployment complete"
|
||||
$lines += " $(Mk $Snap.Phase3.InstallLogExists) DSCInstall.log present"
|
||||
$lines += " $(Mk $Snap.Phase3.InstallComplete) Post-reboot install complete"
|
||||
$lines += ""
|
||||
$lines += " Phase 4: Custom scripts (auto-discovered)"
|
||||
if (-not $Snap.Phase4 -or $Snap.Phase4.Count -eq 0) {
|
||||
$lines += " (no Install-*.log files yet in C:\Logs\SFLD)"
|
||||
} else {
|
||||
foreach ($s in $Snap.Phase4) {
|
||||
$mark = switch ($s.Status) {
|
||||
'done' { '[v]' }
|
||||
'running' { '[.]' }
|
||||
'failed' { '[!]' }
|
||||
'pending' { '[ ]' }
|
||||
default { '[?]' }
|
||||
}
|
||||
$detail = switch ($s.Status) {
|
||||
'done' { "done $(Format-Age $s.Age) ago" }
|
||||
'running' { "running, last update $(Format-Age $s.Age) ago" }
|
||||
'failed' { "FAILED $(Format-Age $s.Age) ago" }
|
||||
'pending' { "pending" }
|
||||
default { "" }
|
||||
}
|
||||
$name = $s.Name.PadRight(22)
|
||||
$lines += " $mark $name $detail"
|
||||
}
|
||||
}
|
||||
$lines += ""
|
||||
$lines += " Phase 5: Final"
|
||||
$lines += " $(Mk $Snap.Phase5.ConsumeCredsTask) SFLD - Consume Credentials task"
|
||||
$lines += ""
|
||||
$sinceSync = ((Get-Date) - $LastSync).TotalSeconds
|
||||
$untilNext = ($NextRetrigger - (Get-Date)).TotalSeconds
|
||||
$lines += " Sync: triggered $(Format-Age $sinceSync) ago | next re-trigger in $(Format-Age $untilNext)"
|
||||
return $lines
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main loop
|
||||
# ============================================================================
|
||||
$qrText = Build-QRCodeText
|
||||
Invoke-IntuneSync
|
||||
$lastSync = Get-Date
|
||||
$nextRetrigger = $lastSync.AddMinutes($RetriggerMinutes)
|
||||
|
||||
while ($true) {
|
||||
$snap = Get-Snapshot
|
||||
|
||||
Clear-Host
|
||||
Write-Host $qrText
|
||||
Write-Host ""
|
||||
foreach ($l in (Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger)) {
|
||||
Write-Host $l
|
||||
}
|
||||
|
||||
# Final state: post-reboot install complete -> exit clean
|
||||
if ($snap.DscInstallComplete) {
|
||||
Write-Host ""
|
||||
Write-Host "All milestones reached. Setup complete." -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Reboot check (boot-loop-safe)
|
||||
$rebootState = Test-RebootState
|
||||
if ($rebootState -eq 'needed') {
|
||||
Write-Host ""
|
||||
Write-Host "Pre-reboot deployment phase complete - REBOOT REQUIRED" -ForegroundColor Yellow
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Re-trigger sync periodically
|
||||
if ((Get-Date) -ge $nextRetrigger) {
|
||||
Write-Host ""
|
||||
Write-Host "Re-triggering Intune sync..." -ForegroundColor Cyan
|
||||
Invoke-IntuneSync
|
||||
$lastSync = Get-Date
|
||||
$nextRetrigger = $lastSync.AddMinutes($RetriggerMinutes)
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $PollSecs
|
||||
}
|
||||
@@ -1,181 +1,76 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
REM sync_intune.bat - Thin launcher for Monitor-IntuneProgress.ps1
|
||||
REM
|
||||
REM All polling, status display, sync triggering, and reboot detection lives in
|
||||
REM the PowerShell monitor. This .bat just handles:
|
||||
REM 1. Self-elevate to admin
|
||||
REM 2. Invoke the monitor (which renders QR + 5-phase status table)
|
||||
REM 3. Branch on the monitor's exit code:
|
||||
REM 0 = post-reboot install complete, "done" message and exit
|
||||
REM 2 = pre-reboot deployment done, prompt for reboot
|
||||
REM else = error, pause so the user can read it
|
||||
REM
|
||||
REM The monitor lives at C:\Enrollment\shopfloor-setup\Shopfloor\Monitor-IntuneProgress.ps1.
|
||||
REM This .bat gets copied to the user's desktop by Run-ShopfloorSetup.ps1, so
|
||||
REM %~dp0 doesn't necessarily point at the shopfloor-setup tree - we use the
|
||||
REM absolute path to find the monitor instead.
|
||||
|
||||
setlocal
|
||||
title Intune Policy Sync
|
||||
|
||||
:: Self-elevate to administrator
|
||||
set "MONITOR=C:\Enrollment\shopfloor-setup\Shopfloor\Monitor-IntuneProgress.ps1"
|
||||
|
||||
REM Self-elevate to administrator
|
||||
net session >nul 2>&1
|
||||
if !errorlevel! neq 0 (
|
||||
if errorlevel 1 (
|
||||
powershell -Command "Start-Process '%~f0' -Verb RunAs"
|
||||
exit /b
|
||||
)
|
||||
|
||||
:: Capture a carriage return character so the polling status line can overwrite
|
||||
:: itself in place (instead of scrolling the QR code off the top of the window).
|
||||
:: The copy /Z trick is the standard batch idiom for getting a literal CR.
|
||||
for /f %%a in ('copy /Z "%~f0" nul') do set "CR=%%a"
|
||||
if not exist "%MONITOR%" (
|
||||
echo ERROR: Monitor not found at:
|
||||
echo %MONITOR%
|
||||
echo.
|
||||
echo Was the shopfloor-setup tree staged correctly?
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%MONITOR%"
|
||||
set "MONITOR_EXIT=%errorlevel%"
|
||||
|
||||
echo.
|
||||
if "%MONITOR_EXIT%"=="0" (
|
||||
echo ========================================
|
||||
echo Intune Policy Sync - %COMPUTERNAME%
|
||||
echo Setup complete - no reboot needed
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
:: Show Intune Device ID and QR code
|
||||
powershell -ExecutionPolicy Bypass -Command ^
|
||||
"$dsreg = dsregcmd /status 2>&1; "^
|
||||
"$line = $dsreg | Select-String DeviceId; "^
|
||||
"if ($line) { "^
|
||||
" $deviceId = $line.ToString().Split(':')[1].Trim(); "^
|
||||
" Write-Host \"Intune Device ID: $deviceId\" -ForegroundColor Cyan; "^
|
||||
" Write-Host ''; "^
|
||||
" $dllPath = 'C:\Enrollment\shopfloor-setup\Shopfloor\QRCoder.dll'; "^
|
||||
" if (Test-Path $dllPath) { "^
|
||||
" Add-Type -Path $dllPath; "^
|
||||
" $gen = New-Object QRCoder.QRCodeGenerator; "^
|
||||
" $data = $gen.CreateQrCode($deviceId, [QRCoder.QRCodeGenerator+ECCLevel]::L); "^
|
||||
" $ascii = New-Object QRCoder.AsciiQRCode($data); "^
|
||||
" $qr = $ascii.GetGraphic(1, [char]0x2588 + [char]0x2588, ' '); "^
|
||||
" Write-Host $qr; "^
|
||||
" } else { "^
|
||||
" Write-Host 'QRCoder.dll not found - skipping QR code' -ForegroundColor Yellow; "^
|
||||
" } "^
|
||||
"} else { "^
|
||||
" Write-Host 'Device not yet Azure AD joined.' -ForegroundColor Yellow; "^
|
||||
"}"
|
||||
|
||||
echo The post-reboot DSC install phase is finished. The device is ready.
|
||||
echo.
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
if "%MONITOR_EXIT%"=="2" (
|
||||
echo ========================================
|
||||
echo Monitoring lockdown progress...
|
||||
echo ========================================
|
||||
echo Step 1: SFLD device configuration
|
||||
echo Step 2: DSC installation
|
||||
echo Step 3: SFLD - Consume Credentials task
|
||||
echo REBOOT REQUIRED
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
:: ---- Polling cadence ----
|
||||
:: An Intune policy pull (Schedule #3 task) typically takes 30-90 seconds end-to-end.
|
||||
:: We poll every POLL_SECS but only RE-TRIGGER sync every RETRIGGER_POLLS iterations
|
||||
:: (so the previous sync has time to actually complete before we kick a new one).
|
||||
:: Old version called do_sync every 15s, which started a fresh sync before the prior
|
||||
:: one had finished and the Intune CSP engine treated each re-trigger as "start over",
|
||||
:: killing in-flight policy application work.
|
||||
set "POLL_SECS=30"
|
||||
set "RETRIGGER_POLLS=6"
|
||||
|
||||
:: ---- STEP 1: Wait for SFLD registry key ----
|
||||
echo [Step 1/3] Waiting for SFLD device configuration...
|
||||
echo Triggering initial Intune sync...
|
||||
call :do_sync
|
||||
set "poll_count=0"
|
||||
|
||||
:poll_sfld
|
||||
reg query "HKLM\Software\GE\SFLD" >nul 2>&1
|
||||
if !errorlevel! equ 0 (
|
||||
echo.
|
||||
goto sfld_done
|
||||
)
|
||||
set /a poll_count+=1
|
||||
set /a remainder=!poll_count! %% !RETRIGGER_POLLS!
|
||||
if !remainder! equ 0 (
|
||||
echo.
|
||||
echo Still waiting after !poll_count! checks - re-triggering sync...
|
||||
call :do_sync
|
||||
)
|
||||
<nul set /p "=!CR! Check #!poll_count! - next check in !POLL_SECS!s... "
|
||||
timeout /t !POLL_SECS! /nobreak >nul
|
||||
goto poll_sfld
|
||||
|
||||
:sfld_done
|
||||
echo [DONE] SFLD device configuration received.
|
||||
|
||||
:: ---- STEP 2: Wait for DSC install completion ----
|
||||
echo.
|
||||
echo [Step 2/3] Waiting for DSC installation to complete...
|
||||
echo Triggering initial Intune sync...
|
||||
call :do_sync
|
||||
set "poll_count=0"
|
||||
|
||||
:poll_dsc
|
||||
set "dsc_ok=0"
|
||||
if exist "C:\LOGS\SFLD\DSCInstall.log" (
|
||||
findstr /C:"Installation completed successfully" "C:\LOGS\SFLD\DSCInstall.log" >nul 2>&1
|
||||
if !errorlevel! equ 0 set "dsc_ok=1"
|
||||
)
|
||||
if !dsc_ok! equ 1 (
|
||||
echo.
|
||||
goto dsc_done
|
||||
)
|
||||
set /a poll_count+=1
|
||||
set /a remainder=!poll_count! %% !RETRIGGER_POLLS!
|
||||
if !remainder! equ 0 (
|
||||
echo.
|
||||
echo Still waiting after !poll_count! checks - re-triggering sync...
|
||||
call :do_sync
|
||||
)
|
||||
<nul set /p "=!CR! Check #!poll_count! - next check in !POLL_SECS!s... "
|
||||
timeout /t !POLL_SECS! /nobreak >nul
|
||||
goto poll_dsc
|
||||
|
||||
:dsc_done
|
||||
echo [DONE] DSC installation completed successfully.
|
||||
|
||||
:: ---- STEP 3: Wait for Consume Credentials scheduled task ----
|
||||
echo.
|
||||
echo [Step 3/3] Waiting for SFLD - Consume Credentials task...
|
||||
echo Triggering initial Intune sync...
|
||||
call :do_sync
|
||||
set "poll_count=0"
|
||||
|
||||
:poll_task
|
||||
schtasks /query /tn "SFLD - Consume Credentials" >nul 2>&1
|
||||
if !errorlevel! equ 0 (
|
||||
echo.
|
||||
goto task_done
|
||||
)
|
||||
set /a poll_count+=1
|
||||
set /a remainder=!poll_count! %% !RETRIGGER_POLLS!
|
||||
if !remainder! equ 0 (
|
||||
echo.
|
||||
echo Still waiting after !poll_count! checks - re-triggering sync...
|
||||
call :do_sync
|
||||
)
|
||||
<nul set /p "=!CR! Check #!poll_count! - next check in !POLL_SECS!s... "
|
||||
timeout /t !POLL_SECS! /nobreak >nul
|
||||
goto poll_task
|
||||
|
||||
:task_done
|
||||
echo [DONE] SFLD - Consume Credentials task found.
|
||||
|
||||
:: ---- COMPLETE ----
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Shopfloor Lockdown complete!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo All 3 steps passed:
|
||||
echo 1. SFLD device configuration
|
||||
echo 2. DSC installation
|
||||
echo 3. Consume Credentials task
|
||||
echo.
|
||||
echo A reboot is required to finalize.
|
||||
echo The pre-reboot deployment phase is complete. You must reboot now to
|
||||
echo start the post-reboot DSC install phase, which downloads device-config.yaml
|
||||
echo and runs the per-app wrappers (Install-eDNC, Install-UDC, Install-VCRedists,
|
||||
echo Install-OpenText, etc).
|
||||
echo.
|
||||
choice /c YN /m "Reboot now"
|
||||
if !errorlevel! equ 1 shutdown /r /t 5
|
||||
exit /b
|
||||
if errorlevel 2 (
|
||||
echo Cancelled - reboot manually when ready.
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
shutdown /r /t 5
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
:: ---- Subroutine: trigger Intune sync ----
|
||||
:do_sync
|
||||
powershell -ExecutionPolicy Bypass -Command ^
|
||||
"$enrollPath = 'HKLM:\SOFTWARE\Microsoft\Enrollments'; "^
|
||||
"Get-ChildItem $enrollPath -ErrorAction SilentlyContinue | ForEach-Object { "^
|
||||
" $provider = (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).ProviderID; "^
|
||||
" if ($provider -eq 'MS DM Server') { "^
|
||||
" $id = $_.PSChildName; "^
|
||||
" $taskPath = \"\Microsoft\Windows\EnterpriseMgmt\$id\\\"; "^
|
||||
" Get-ScheduledTask -TaskPath $taskPath -ErrorAction SilentlyContinue | "^
|
||||
" Where-Object { $_.TaskName -match 'Schedule #3' } | "^
|
||||
" ForEach-Object { Start-ScheduledTask -InputObject $_ }; "^
|
||||
" } "^
|
||||
"}" >nul 2>&1
|
||||
exit /b
|
||||
echo ERROR: Monitor exited with code %MONITOR_EXIT%
|
||||
pause
|
||||
exit /b %MONITOR_EXIT%
|
||||
|
||||
Reference in New Issue
Block a user