AutoPilot: Post ESP – Installation des applications Win32 sous Intune

par | Juin 11, 2026 | Autopilot, Intune, Windows 10, Windows 11 | 0 commentaires

Dans les environnements d’entreprise, nous devons répondre à de nombreuses exigences en matière de gestion des applications. L’un des défis les plus courants consiste à contrôler le moment d’installation des applications durant le processus d’enrôlement, notamment avec Windows Autopilot.

Prenons un exemple : vous avez comme prérequis que l’application « X » soit disponible dès que le bureau Windows est affiché (Windows Desktop loaded) et prête à être utilisée par l’utilisateur à l’issue du déploiement Autopilot. L’application ne doit être installée ni trop tôt afin de ne pas impacter l’expérience d’enrôlement, ni trop tard afin de garantir sa disponibilité immédiate dès l’arrivée sur le bureau.

L’objectif de ce blog est de s’assurer que l’application X ne s’installe qu’une fois l’ESP (Enrollment Status Page) terminée, à travers deux scripts PowerShell déployés via une application Win32.

Script 2 : contrôle l’installation de l’application en fonction de l’état du processus ESP.

Script 1 : copie les fichiers sources et crée une tâche planifiée qui exécutera le Script 2.

Actions effectuées

• Crée le dossier *C:\ProgramData\XXX\XXXX* s’il n’existe pas.
• Copie tous les fichiers du package (MSI/EXE + 2 scripts) dans ce dossier.
• Met à jour les fichiers du package lorsqu’une nouvelle version du MSI/EXE est détectée.
• Crée une tâche planifiée nommée « XXXX_PostESP_Install » afin d’installer notre application (XXXX) lors de la première connexion de l’utilisateur.


FichierRole
Copy-XXXX-task.ps1Crée le répertoire d’installation, remplace le fichier d’installation lorsqu’une nouvelle version est détectée, puis crée la tache planifiée qui sera exécutée à la suite de l’ouverture du session Windows.
Install_XXXX_PostESP.ps1Vérifie que l’Enrollment Status Page (ESP) est terminée et, le cas échéant, que la configuration de WHFB est également finalisée.
Une fois ces vérifications effectuées, le script lance l’installation de l’application
XXXX.msi/.exeFichier d’installation de l’application (quel que soit son format .msi ou .exe)

NB: les trois fichiers listés dans le tableau, doivent etre présents ensemble dans le meme répertoire ava,t la création du package Intune.

Ces deux scripts seront encapsuler dans fichier .intunewin puis le crée une application WIN32 comme suit:
Program:
Install command:
Powershell.exe -ExecutionPolicy Bypass .\Copy-XXXX-task.ps1
Uninstall command: cmd.exe /c
Rule type: File
Path: C:\ProgramData\XXXX\XXXX
File or folder : « Fichier d’installation de l’application MSI/EXE »
Detection method: File or folder exists

La méthode de détection et les deux scripts peuvent être modifier selon vos besoins

Exemple fichier log génerer par le script d’installation:

Copy-XXXX-task.ps1 :

# ============================================================
# 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.2
# ============================================================

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

# 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 $XXXX $InstallerName
$ScriptDest    = Join-Path $XXXX $ScriptName

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

# Check if folder exists
if (-Not (Test-Path $XXXX)) {
    #
    try {
        New-Item -ItemType Directory -Path $XXXX -Force | Out-Null
        Write-Log "Folder $XXXX created successfully."
    } catch {
        Write-Log "Error: Failed to create folder $XXXX. $_"
        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 $XXXX -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 "$XXXX\*" -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."
}

# 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 {
    # Define scheduled task action
    $Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptDest`""

    # Define trigger
    $TriggerAtLogon = New-ScheduledTaskTrigger -AtLogOn

    # Define task principal (run as SYSTEM)
    $Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

    # Define settings
    $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Days 0)

    # Register the task
    try {
        Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $TriggerAtLogon -Principal $Principal -Settings $Settings -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
    }
}

Install_XXXX_PostESP.ps1:
• Nombre maximal de tentatives : 30
• Délai entre chaque tentative : 5 minutes
• Si les 30 tentatives échouent : la tâche planifiée reste active et une nouvelle tentative sera effectuée lors de la prochaine connexion de l’utilisateur.

# ============================================================
# 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.2
# ============================================================

$XXXX  = "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.exe"

# Check MSI file...
$InstallerSourceMSI = Get-ChildItem -Path $XXXX -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 $XXXX. Exiting." -ErrorAction SilentlyContinue
    exit 1
}

$InstallerName = $InstallerSourceMSI.Name
$InstallerPath = Join-Path $XXXX $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 : skip if XXXX is already installed
# -------------------------------------------------------
if (Test-Path $filePath) {
    # Normalise FileVersion
    $rawFileVersion = (Get-Item $filePath).VersionInfo.FileVersion -replace '\s.*$', ''

    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

# ---------- 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."
        break
    }

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

} while ($espWaitSeconds -lt $espMaxWait)

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

# This line must be updated with the appropriate code for your application!!!!
$msiArgs = "/i `"$InstallerPath`" /qn /l*v `"$LogPath`" " +
           "STRICTENFORCEMENT=1 " +
           "CLOUDNAME=XXXX " +
           "USERDOMAIN=XXXX " +
           "POLICYTOKEN=391083766XXXXXXXXXXXXXXXXXX " +
           "REBOOT=ReallySuppress"

$maxAttempts = 30
$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 5 min..." ; Start-Sleep -Seconds 300 }
            1619 { Write-Log "Package could not be opened (1619) - retrying in 5 min..." ; Start-Sleep -Seconds 300 }
            default {
                Write-Log "Unexpected exit code $($process.ExitCode) - retrying in 5 min..."
                Start-Sleep -Seconds 300
            }
        }
    } catch {
        Write-Log "ERROR launching msiexec: $_"
        Start-Sleep -Seconds 300
    }
}

# -------------------------------------------------------
# 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 "$XXXX\*" -Exclude $InstallerName -Confirm:$false -Recurse -ErrorAction Stop
    exit 0
} else {
    Write-Log "FAILED after $maxAttempts attempts - scheduled task remains for next logon retry."
    exit 1
}

0 commentaires

Soumettre un commentaire

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