AutoPilot: Post ESP – Retour d’expérience et améliorations de la solution (v2)

par | Août 22, 2026 | Intune, Windows 10, Windows 11 | 0 commentaires

Contexte

Dans un précédent article, nous avons présenté une solution basée sur deux scripts PowerShell permettant d’installer une application Win32 après la fin de l’ESP (Enrollment Status Page) lors d’un enrôlement Windows Autopilot.

Après déploiement en production, deux comportements inattendus ont été identifiés. Cet article décrit ces problèmes et les corrections apportées dans la version 2 des scripts.

Problèmes identifiés

Problème 1 — La tâche planifiée se déclenchait sur la session defaultuser0

Dans un scénario Autopilot Pre-Provisioning (White Glove), la phase Device Setup est exécutée par le processus CloudExperienceHostBroker.exe. À la fin de cette phase, ce processus déclenche un redémarrage automatique du device.

Après ce redémarrage, Windows ouvre automatiquement une session sous le compte defaultuser0 et affiche la page Reseal — la page de fin du Pre-Provisioning qui invite le technicien à sceller le device avant sa livraison à l’utilisateur.

Le point clé : la session defaultuser0 est déjà active à ce stade, même si personne n’a encore interagi avec le device. La tâche planifiée avec le trigger AtLogOn se déclenchait donc immédiatement après le redémarrage, pendant que le device attendait simplement sur la page Reseal sans aucune action utilisateur.

Autopilot Pre-Provisioning — Device Setup
        │
        ▼
CloudExperienceHostBroker.exe termine la phase Device Setup
        │
        ▼
Redémarrage automatique du device
        │
        ▼
Windows ouvre la session defaultuser0
Device affiche la page Reseal (en attente du technicien)
        │
        ▼  ← à ce moment, defaultuser0 est déjà connecté
Trigger AtLogOn se déclenche immédiatement  ❌
        │
        ▼
Script d'installation démarre en arrière-plan
(sans que personne n'ait cliqué quoi que ce soit)
        │
        ▼
Boucle ESP → attend jusqu'à 5h inutilement

Solution _ Problème 1 : Guard defaultuser0

Un contrôle a été ajouté au début du script pour arrêter immédiatement l’installation si la session détectée est defaultUser0

# ------------------------------------------------------------------
# 0. Guard 1 : defaultuser0 → exit immédiat ──
# ------------------------------------------------------------------
$currentUser = (Get-WmiObject -Class Win32_ComputerSystem -ErrorAction SilentlyContinue).UserName
Write-Log "Current logged user : $currentUser"

if ([string]::IsNullOrEmpty($currentUser) -or $currentUser -like "*defaultuser0*") {
    Write-Log "defaultuser0 session - exiting immediately. Task will retry at real user logon."
    exit 0 
}
defaultuser0 logon
        │
        ▼
Guard Win32_ComputerSystem.UserName = "defaultuser0"
        │
        ▼
exit 0 immédiat (< 1 seconde)
        │
        ▼
Tâche planifiée reste active → réessaie au vrai logon utilisateur ✅

Problème 2 — Timeout de la boucle ESP sans vérification de l’état réel

La boucle d’attente ESP de la version 1 utilisait la condition suivante :

do {
    # vérifie les checks ESP
    if ($c1 -and $c2 -and $c3 -and $c4) { break }

    Start-Sleep -Seconds $pollInterval
    $espWaitSeconds += $pollInterval

} while ($espWaitSeconds -lt $espMaxWait)

# ← installation lancée ici, sans savoir pourquoi on est sorti de la boucle

Si le timeout de 5h était atteint sans que les checks ESP soient validés, le script sortait de la boucle et continuait vers l’installation — exactement comme si l’ESP était terminée.

Cause

La condition while ($espWaitSeconds -lt $espMaxWait) provoque deux sorties possibles :

SortieRaisonComportement v1
breakTous les checks $true✅ Installation correcte
Fin de boucleTimeout 5h, checks toujours $false❌ Installation quand même

Solution _ Problème 2 : Flag $espCompleted

Un flag explicite permet de distinguer les deux cas de sortie :

$espCompleted = $false  # ← nouveau flag

do {
    $c1 = Test-IsSyncDone
    $c2 = Test-SidecarCompleted
    $c3 = Test-HasProvisioningCompleted
    $c4 = Test-TrackingPoliciesCreated

    Write-Log "ESP check at ${espWaitSeconds}s → IsSyncDone=$c1 | Sidecar=$c2 | Provisioning=$c3 | Policies=$c4"

    if ($c1 -and $c2 -and $c3 -and $c4) {
        $espCompleted = $true  # ← toutes conditions validées
        break
    }

    Start-Sleep -Seconds $pollInterval
    $espWaitSeconds += $pollInterval

} while ($espWaitSeconds -lt $espMaxWait)

# ── Vérifier POURQUOI on est sorti ──
if (-not $espCompleted) {
    Write-Log "TIMEOUT: ESP did not complete after $($espMaxWait/3600)h"
    Write-Log "Checks → IsSyncDone=$c1 | Sidecar=$c2 | Provisioning=$c3 | Policies=$c4"
    Write-Log "Aborting - scheduled task remains for next logon retry."
    exit 1  # ← ne pas installer, conditions pas remplies
}

Write-Log "ESP completed after ${espWaitSeconds}s - proceeding with installation."
# ← ici on est certain que tous les checks sont true
Boucle ESP démarre
        │
        ├── Checks tous true → $espCompleted = true → break → Installe ✅
        │
        └── Timeout 5h → $espCompleted = false → exit 1
                                │
                                ▼
                        Tâche planifiée reste active
                        Retry au prochain logon utilisateur ✅

Récapitulatif des corrections

#ProblèmeVersion 1Version 2
1Tâche déclenchée sur defaultuser0Trigger AtLogOnConsoleConnect + Guard ✅
2Timeout ESP → installation forcéeInstalle quand même ❌Flag $espCompletedexit 1

Script 1

# ============================================================
# Install_XXXX_PostESP.ps1 - Payload Script (runs at logon)
# Purpose : Wait for Autopilot ESP Phase 3 to finish, then
#           install XXXX MSI with retry logic.
#
# Triggered by : Scheduled Task "XXXXInstallIfMissing"
# RunAs        : SYSTEM
# Author       : Lazher YAAKOUBI
# Version      : 2.7.3
# ============================================================

$XXXXFolder  = "C:\ProgramData\XXXX\XXXX_Source"
$LogPath        = "$env:WINDIR\Temp\install_XXXX.log"
$LogFile        = "$env:WINDIR\Temp\XXXX-install-attempt.log"
$TaskName       = "XXXXInstallIfMissing"
$filePath       = "C:\Program Files\XXXX\XXXX"

# Check MSI file...
$InstallerSourceMSI = Get-ChildItem -Path $XXXXFolder -Filter "*.msi" | Select-Object -First 1
if (-not $InstallerSourceMSI) {
    
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') : ERROR - No MSI found in $XXXXFolder. Exiting." -ErrorAction SilentlyContinue
    exit 1
}

$InstallerName = $InstallerSourceMSI.Name
$InstallerPath = Join-Path $XXXXFolder $InstallerName

# e.g. "XXXX-windows-4.2.0.172-x64" -> "4.2.0.172"
$MSIVersion = [System.IO.Path]::GetFileNameWithoutExtension($InstallerPath) `
              -replace 'XXXX-windows-', '' `
              -replace '-x64', '' `
              -replace '-corp', '' `
              -replace '-[a-zA-Z].*$', ''   # catch any other suffix

function Write-Log {
    param([string]$Message)
    $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') : $Message"
    Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue
}

# ------------------------------------------------------------------
# 0. Guard 1 : defaultuser0 → exit immédiat  ──
# ------------------------------------------------------------------
$currentUser = (Get-WmiObject -Class Win32_ComputerSystem -ErrorAction SilentlyContinue).UserName
Write-Log "Current logged user : $currentUser"

if ([string]::IsNullOrEmpty($currentUser) -or $currentUser -like "*defaultuser0*") {
    Write-Log "defaultuser0 session - exiting immediately. Task will retry at real user logon."
    exit 0 
}

# -------------------------------------------------------
# 1. Guard 2 : skip if XXXX is already installed
# -------------------------------------------------------
if (Test-Path $filePath) {
    if ($rawFileVersion -eq $MSIVersion) {
     #>
        Write-Log "XXXX $rawFileVersion already installed - removing scheduled task and exiting."
       
        if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) 
        {
            Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
            Write-Log "Scheduled task '$TaskName' removed."
        } else 
            {
            Write-Log "Scheduled task '$TaskName' does not exist."
            }

        exit 0
        <#
    } else {
        Write-Log "Installed version ($rawFileVersion) differs from MSI version ($MSIVersion) - proceeding with installation."
    }
    #>
}

Write-Log "Waiting for ESP completion (using IME-equivalent checks)..."

$espWaitSeconds = 0
$espMaxWait     = 18000  # 5 h
$pollInterval   = 15     # seconds between checks
$espCompleted   = $false

# ---------- Check 1: IsSyncDone ----------
function Test-IsSyncDone {
    $enrollments = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Enrollments" -ErrorAction SilentlyContinue
    foreach ($enrollment in $enrollments) {
        $providerID = (Get-ItemProperty -Path $enrollment.PSPath -Name "ProviderID" -ErrorAction SilentlyContinue).ProviderID
        if ($providerID -ne "MS DM Server") { continue }

        $firstSyncPath = Join-Path $enrollment.PSPath "FirstSync"
        $userSidKeys = Get-ChildItem -Path $firstSyncPath -ErrorAction SilentlyContinue
        foreach ($sidKey in $userSidKeys) {
            $isSyncDone = (Get-ItemProperty -Path $sidKey.PSPath -Name "IsSyncDone" -ErrorAction SilentlyContinue).IsSyncDone
            if ($isSyncDone -eq 1) { return $true }
        }
    }
    return $false
}

# ---------- Check 2: Sidecar InstallationState ----------update must be performed to resolve issues related to failed autopilot device!!!
function Test-SidecarCompleted {
    $enrollments = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Enrollments" -ErrorAction SilentlyContinue
    foreach ($enrollment in $enrollments) {
        $sidecarPath = Join-Path $enrollment.PSPath "PolicyProviders\Sidecar"
        $state = (Get-ItemProperty -Path $sidecarPath -Name "InstallationState" -ErrorAction SilentlyContinue).InstallationState
        if ($state -eq "Completed") { return $true }
    }
    # If Sidecar key doesn't exist at all, it's not blocking ESP
    return $true
}

# ---------- Check 3: HasProvisioningCompleted (WMI) ----------
function Test-HasProvisioningCompleted {
    try {
        $result = Get-WmiObject -Namespace "root\cimv2\mdm\dmmap" `
            -Query "SELECT HasProvisioningCompleted FROM MDM_EnrollmentStatusTracking_Setup01" `
            -ErrorAction Stop
        if ($null -ne $result -and $result.HasProvisioningCompleted -eq $true) { return $true }
    } catch {
        return $true
    }
    return $false
}

# ---------- Check 4: TrackingPoliciesCreated (WMI) ----------
function Test-TrackingPoliciesCreated {
    try {
        $result = Get-WmiObject -Namespace "root\cimv2\mdm\dmmap" `
            -Query "SELECT TrackingPoliciesCreated FROM MDM_EnrollmentStatusTracking_PolicyProviders03_01" `
            -ErrorAction Stop
        if ($null -ne $result -and $result.TrackingPoliciesCreated -eq $true) { return $true }
    } catch {
        return $true
    }
    return $false
}

# ---------- Check 5: WWAHost.exe Running ----------
function Test-WWAHostVisible {
    $wwa = Get-Process -Name "WWAHost" -ErrorAction SilentlyContinue |
           Where-Object { $_.SessionId -gt 0 }
    return ($null -ne $wwa)
}

# ---------- Main polling loop ----------
do {
    $c1 = Test-IsSyncDone
    $c2 = Test-SidecarCompleted
    $c3 = Test-HasProvisioningCompleted
    $c4 = Test-TrackingPoliciesCreated
    $c5 = Test-WWAHostVisible

    Write-Log ("ESP/WHfB status at ${espWaitSeconds}s - " +
               "IsSyncDone=$c1 | SidecarCompleted=$c2 | " +
               "HasProvisioningCompleted=$c3 | TrackingPoliciesCreated=$c4 | " +
               "WWAHostVisible=$c5")

    if ($c1 -and $c2 -and $c3 -and $c4 -and (-not $c5)) {
        Write-Log "All checks passed - ESP done and WWAHost closed. Proceeding after ${espWaitSeconds}s."
        $espCompleted = $true
        break
    }

    Start-Sleep -Seconds $pollInterval
    $espWaitSeconds += $pollInterval

} while ($espWaitSeconds -lt $espMaxWait)

if (-not $espCompleted)
{
    Write-Log "TIMEOUT ESP did not completed after $($espMaxWait/3600)H - IsSyncDone = $c1, SidecarCompleted = $c2, HasProvisioningCompleted = $c3, TrackingPoliciesCreated = $c4"
    Write-Log "Aborting installation - scheduled task remains for next logon retry..."
    exit 1
}

Write-Log "ESP completed after ${espWaitSeconds}s - proceeding with installation..."

# -------------------------------------------------------
# 2. INSTALL XXXX WITH RETRY LOGIC
# -------------------------------------------------------
if (-Not (Test-Path $InstallerPath)) {
    Write-Log "ERROR: Installer not found at $InstallerPath"
    exit 1
}

$msiArgs = "/i `"$InstallerPath`" /qn /l*v `"$LogPath`" " +
           "STRICTENFORCEMENT=1 " +
           "CLOUDNAME=XXXXX " +
           "USERDOMAIN=XXXXX " +
           "POLICYTOKEN=343837323A333A30323236653933352D383433632D343531352D396338312D326333383135613036613665 " +
           "REBOOT=ReallySuppress"

$maxAttempts = 10
$attempts    = 0
$success     = $false

while (-not $success -and $attempts -lt $maxAttempts) {
    $attempts++
    Write-Log "Installation attempt $attempts of $maxAttempts"

    try {
        $process = Start-Process "msiexec.exe" `
            -ArgumentList $msiArgs `
            -Wait -PassThru -NoNewWindow -ErrorAction Stop

        Write-Log "msiexec exit code: $($process.ExitCode)"

        switch ($process.ExitCode) {
            0    { $success = $true ; Write-Log "Installation succeeded." }
            3010 { $success = $true ; Write-Log "Installation succeeded (reboot required - suppressed)." }
            1618 { Write-Log "Another MSI installation in progress (1618) - retrying in 3 min..." ; Start-Sleep -Seconds 180 }
            1619 { Write-Log "Package could not be opened (1619) - retrying in 3 min..." ; Start-Sleep -Seconds 180 }
            default {
                Write-Log "Unexpected exit code $($process.ExitCode) - retrying in 3 min..."
                Start-Sleep -Seconds 180
            }
        }
    } catch {
        Write-Log "ERROR launching msiexec: $_"
        Start-Sleep -Seconds 180
    }
}

# -------------------------------------------------------
# 3. FINAL STATUS
# -------------------------------------------------------
if ($success) {
    Write-Log "XXXX installed successfully - removing scheduled task."
    Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue

    Write-Log "Cleaning up XXXX_Source folder (full removal)..."
    #Remove-Item -Path "$XXXXFolder\*" -Exclude $InstallerName -Confirm:$false -Recurse -ErrorAction Stop
    Remove-Item -Path "$XXXXFolder" -Confirm:$false -Recurse -ErrorAction Stop
    exit 0
} else {
    Write-Log "FAILED after $maxAttempts attempts - scheduled task remains for next logon retry."
    exit 1
} 

Script 2 Copy

# ============================================================
# Copy-XXXX-task.ps1 - Payload Script (runs at AutoPilot phase 2)
# Purpose : Update source files if needed and create scheduled task.
#
# Triggered by : Intune application deployement services"
# RunAs        : SYSTEM
# Author       : Lazher YAAKOUBI
# Version      : 2.7.3
# ============================================================

# Folder where XXXX installation files must be copied
$XXXXFolder = "C:\ProgramData\XXXX\XXXX_Source"
$LogFile  = "$env:WINDIR\Temp\XXXX-Copy-TaskCreation.log"
#$filePath = "C:\Program Files\XXXX\"

# Find the MSI and script files
$InstallerSourceMSI = Get-ChildItem -Path $PSScriptRoot -Filter "*.msi" | Select-Object -First 1
$InstallerSourcePS1 = Get-ChildItem -Path $PSScriptRoot -Filter "*.ps1" | Where-Object { $_.Name -like "*Install_XXXX*" } | Select-Object -First 1

function Write-Log {
    param([string]$Message)
    $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') : $Message"
    Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue
}

# Check source files existance MSI&PS1
if (-not $InstallerSourceMSI) {
    Write-Log "Error: No MSI file found in $PSScriptRoot."
    exit
}

if (-not $InstallerSourcePS1) {
    Write-Log "Error: No installation script found in $PSScriptRoot."
    exit
}

$InstallerName = $InstallerSourceMSI.Name
$InstallerSource = $InstallerSourceMSI.FullName
$ScriptName = $InstallerSourcePS1.Name
$ScriptSource = $InstallerSourcePS1.FullName

$InstallerDest = Join-Path $XXXXFolder $InstallerName
$ScriptDest    = Join-Path $XXXXFolder $ScriptName

# Name of the scheduled task
$TaskName = "XXXXInstallIfMissing"

# Check if folder exists
if (-Not (Test-Path $XXXXFolder)) {
    #
    try {
        New-Item -ItemType Directory -Path $XXXXFolder -Force | Out-Null
        Write-Log "Folder $XXXXFolder created successfully."
    } catch {
        Write-Log "Error: Failed to create folder $XXXXFolder. $_"
        exit
    }

    Copy-Item -Path $InstallerSource -Destination $InstallerDest -Force
    Copy-Item -Path $ScriptSource    -Destination $ScriptDest    -Force
} else {
    Write-Log "XXXX folder already exists. Checking for updates..."

    # Update MSI if needed
    $ExistingMSI = Get-ChildItem -Path $XXXXFolder -Filter "*.msi" -File
    if ($ExistingMSI) {
        $ExistingMSIVersion = [System.IO.Path]::GetFileNameWithoutExtension($ExistingMSI.Name) -replace 'XXXX-windows-', '' -replace '-x64', '' -replace '-corp', ''
        $NewMSIVersion      = [System.IO.Path]::GetFileNameWithoutExtension($InstallerName)    -replace 'XXXX-windows-', '' -replace '-x64', '' -replace '-corp', ''

        # Version comparison
        if (([version]$ExistingMSIVersion) -lt ([version]$NewMSIVersion)) {
            Write-Log "Newer MSI version detected ($NewMSIVersion). Updating..."
            Remove-Item -Path "$XXXXFolder\*" -Force
            Copy-Item -Path $InstallerSource -Destination $InstallerDest -Force
            Copy-Item -Path $ScriptSource -Destination $InstallerDest -Force
        } else {
            Write-Log "Existing MSI version ($ExistingMSIVersion) is up to date."
        }
    } else {
        Copy-Item -Path $InstallerSource -Destination $InstallerDest -Force
    }

    # Always overwrite the script so it stays in sync with the MSI
    Copy-Item -Path $ScriptSource -Destination $ScriptDest -Force
    Write-Log "Installation script updated."
}

$TaskXML = @"
<Task xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task" version="1.3">
<Triggers>
    <LogonTrigger>
        <Enabled>true</Enabled>
    </LogonTrigger>
</Triggers>
<Principals>
    <Principal id="Author">
        <UserId>SYSTEM</UserId>
        <RunLevel>HighestAvailable</RunLevel>
    </Principal>
</Principals>
<Settings>
        <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
        <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
        <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
        <AllowHardTerminate>true</AllowHardTerminate>
        <StartWhenAvailable>true</StartWhenAvailable>
        <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
        <StopOnIdleEnd>true</StopOnIdleEnd>
        <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
    <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
    <Priority>7</Priority>
</Settings>
<Actions Context="Author">
    <Exec>
        <Command>powershell.exe</Command>
        <Arguments>-ExecutionPolicy Bypass -WindowStyle Hidden -File "$ScriptDest"</Arguments>
    </Exec>
</Actions>
</Task>
"@

# Check if the scheduled task already exists
$ExistingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($ExistingTask) {
    Write-Log "Scheduled task '$TaskName' already exists. Skipping creation."
} else {
    try {
        Register-ScheduledTask -TaskName $TaskName -Xml $TaskXML -Force -ErrorAction Stop
        Write-Log "Scheduled task '$TaskName' created successfully." #| Out-File -FilePath "$env:WINDIR\Temp\TaskCreation.log" -Append
    } catch {
        Write-Log "Failed to create scheduled task: $_" #| Out-File -FilePath "$env:WINDIR\Temp\TaskCreation.log" -Append
    }
} 

0 commentaires

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *