From 66d13d8ad07a7d2c746acfe59ed4009e6bc77796 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 9 Apr 2026 10:56:20 -0400 Subject: [PATCH] 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: " 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) --- .../Shopfloor/Monitor-IntuneProgress.ps1 | 453 ++++++++++++++++++ .../shopfloor-setup/Shopfloor/sync_intune.bat | 223 +++------ 2 files changed, 512 insertions(+), 164 deletions(-) create mode 100644 playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1 diff --git a/playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1 b/playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1 new file mode 100644 index 0000000..a24cf73 --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/Monitor-IntuneProgress.ps1 @@ -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: " 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 +} diff --git a/playbook/shopfloor-setup/Shopfloor/sync_intune.bat b/playbook/shopfloor-setup/Shopfloor/sync_intune.bat index a2b0ddf..3c01bf8 100644 --- a/playbook/shopfloor-setup/Shopfloor/sync_intune.bat +++ b/playbook/shopfloor-setup/Shopfloor/sync_intune.bat @@ -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" - -echo. -echo ======================================== -echo Intune Policy Sync - %COMPUTERNAME% -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. -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 ======================================== -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 ( +if not exist "%MONITOR%" ( + echo ERROR: Monitor not found at: + echo %MONITOR% echo. - goto sfld_done + echo Was the shopfloor-setup tree staged correctly? + pause + exit /b 1 ) -set /a poll_count+=1 -set /a remainder=!poll_count! %% !RETRIGGER_POLLS! -if !remainder! equ 0 ( + +powershell -NoProfile -ExecutionPolicy Bypass -File "%MONITOR%" +set "MONITOR_EXIT=%errorlevel%" + +echo. +if "%MONITOR_EXIT%"=="0" ( + echo ======================================== + echo Setup complete - no reboot needed + echo ======================================== echo. - echo Still waiting after !poll_count! checks - re-triggering sync... - call :do_sync -) -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 The post-reboot DSC install phase is finished. The device is ready. echo. - goto dsc_done + pause + exit /b 0 ) -set /a poll_count+=1 -set /a remainder=!poll_count! %% !RETRIGGER_POLLS! -if !remainder! equ 0 ( + +if "%MONITOR_EXIT%"=="2" ( + echo ======================================== + echo REBOOT REQUIRED + echo ======================================== echo. - echo Still waiting after !poll_count! checks - re-triggering sync... - call :do_sync -) -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 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. - goto task_done + choice /c YN /m "Reboot now" + if errorlevel 2 ( + echo Cancelled - reboot manually when ready. + pause + exit /b 0 + ) + shutdown /r /t 5 + exit /b 0 ) -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 -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. -choice /c YN /m "Reboot now" -if !errorlevel! equ 1 shutdown /r /t 5 -exit /b - -:: ---- 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%