Le blog technique

Toutes les astuces #tech des collaborateurs de PI Services.

#openblogPI

Retrouvez les articles à la une

PoshTorch : module Powershell pour exporter des métriques Prometheus

Plusieurs modules Powershell développés par la communauté autour du besoin d’exporter des métriques au format Prometheus depuis des scripts Powershell existent déjà mais aucun ne répondait précisément à mon besoin, faire le strict minimum à l’aide de la syntaxe la plus simple et claire possible:

  • Créer des métriques
  • Leur ajouter une ou plusieurs valeurs avec ou sans labels
  • Formater le résultat au format Prometheus
  • Exporter le résultat dans le terminal (StdOut), vers un fichier texte (avec écriture atomique) qui puisse être lu par un Node Exporter Prometheus local ou enfin vers une PushGateway.

Nul besoin d’ajouter la gestion de l’affichage des métriques directement en http local dans ce module : cela rajoute une forte couche de complexité non liée au besoin initial et de vrais risques de sécurité, alors ce rôle peut très bien être assumé par un module dédié comme le très complet Pode combiné à la sortie en StdOut comme nous le verrons plus loin.

J’ajouterais qu’il s’agissait aussi d’un bon prétexte pour tester plus sérieusement les classes de Powershell.

Trève de bavardages, commençons par le lien vers le repo GitHub du module (pas encore disponible sur la PSGallery) : https://github.com/Cyr-Az/poshtorch

Voyons maintenant comment fonctionne le module :

Avant d’exporter les données, il est nécessaire de créer les objets correspondants en et de leur assigner des valeurs. Le module s’appuie sur deux commandes principales pour ces opérations :

  • New-PrometheusMetric : Initialise l’objet métrique avec ses métadonnées de base (nom, description et type). Le nom de la métrique est automatiquement validé pour respecter les standards de Prometheus (caractères alphanumériques et underscores uniquement). Cette commande supporte les paramètres suivants :
    • Name : le nom technique de la métrique
    • Help : une description textuelle lisible par un humain
    • Type : le type de métrique, limité aux valeurs conformes à Prometheus (counter, gauge, histogram ou summary).
  • Add-PrometheusMetricValue : Attache une valeur numérique à une métrique préalablement créée. Cette fonction peut être appelée plusieurs fois sur la même métrique pour générer des séries temporelles multiples (Time Series). Elle supporte les paramètres suivants :
    • Metric : l’objet métrique à modifier, généralement passé via le pipeline
    • Value : la valeur numérique
    • Labels : paramètre optionnel acceptant une hashtable @{ clé = « valeur » } pour définir les étiquettes associées à cette valeur.

Une fois les objets métriques créés via New-PrometheusMetric et Add-PrometheusMetricValue, le module propose trois fonctions distinctes pour restituer  (exporter) la donnée selon vos besoins :

  • Out-PrometheusMetric : Convertit l’objet PowerShell en une chaîne de caractères (texte brut) respectant le format d’exposition Prometheus (version 0.0.4). Elle est utilisée pour le debug dans la console ou pour transmettre le texte à un serveur HTTP. Elle supporte les paramètres suivants :
    • Metric : accepte un ou plusieurs objets de métriques, généralement via le pipeline.
  • Export-PrometheusFile : Sauvegarde les métriques dans un fichier texte (souvent avec l’extension .prom). Cette méthode est conçue pour être couplée au Textfile Collector de l’agent Node Exporter. La fonction utilise un fichier temporaire qu’elle remplace de manière atomique (uniquement en Powershell 7+) afin d’éviter que Prometheus ne scrape un fichier en cours d’écriture. Elle supporte les paramètres suivants :
    • Metric : accepte un ou plusieurs objets de métriques, généralement via le pipeline.
    • Path : le chemin complet du fichier de destination .
  • Export-PrometheusPushgateway : Envoie directement les métriques à une instance Prometheus Pushgateway via une requête http(s). C’est l’approche recommandée pour les scripts éphémères (ex: tâches planifiées ou scripts de sauvegarde) qui ne peuvent pas être interrogés activement par Prometheus. Elle supporte les paramètres suivants :
    • Metric : accepte un ou plusieurs objets de métriques, généralement via le pipeline.
    • GatewayUrl : l’adresse de la Pushgateway, ex: http://localhost:9091
    • Job : le nom du job ciblé 
    • Instance : optionnel, pour spécifier le nom de l’instance
    • Method : PUT pour remplacer le précédent batch de métriques recu par la PushGateway (valeur par défaut) ou POST pour ajouter les données au précédent batch de métriques.

Terminons enfin cet article par quelques exemples d’utilisation illustrant le fonctionnement du module dans des scénarios d’administration courants :

Utilisation avec le pipeline PowerShell

Les fonctions du module sont conçues pour supporter le pipeline. Cela permet d’enchaîner la création de la métrique, l’ajout de la valeur et l’export sans nécessiter de variables intermédiaires, ce qui est particulièrement adapté pour l’export de métriques simples, pour une seule instance/un seul objet ou sans label particulier.

Dans l’exemple ci-dessous, le script récupère le nombre de comptes Active Directory verrouillés et l’écrit directement dans un fichier .prom destiné à être lu par le Textfile Collector d’un Node Exporter :

Import-Module -Name ActiveDirectory

Import-Module -Name PoshTorch

# Récupération de la donnée

$lockedAccounts = (Get-ADUser -Filter {LockedOut -eq $true}).Count

# Création, ajout de la valeur et export vers le fichier

New-PrometheusMetric -Name 'ad_locked_accounts_total' -Help 'Comptes AD verrouillés' -Type gauge |

    Add-PrometheusMetricValue -Value $lockedAccounts |

    Export-PrometheusFile -Path 'C:\Metrics\ad_health.prom'

Gestion des labels et des séries multiples

Prometheus utilise des labels pour ajouter de la dimension aux métriques, ce qui permet de stocker plusieurs séries temporelles (Time Series) sous un même nom. PoshTorch gère ces labels via les hashtables standards de PowerShell.

L’exemple suivant vérifie l’état de plusieurs services Windows. La métrique est créée une seule fois, puis alimentée itérativement avant d’être envoyée à une PushGateway :

Import-Module -Name PoshTorch

$criticalServices = @("wuauserv", "Spooler", "WinRM")

# Initialisation de l'objet métrique

$serviceMetric = New-PrometheusMetric -Name 'windows_service_status' `

                                      -Help '1 si en cours, 0 si arrêté' `

                                      -Type gauge

foreach ($serviceName in $criticalServices)

{   

    $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue

    $isUp = if ($service.Status -eq 'Running') { 1 } else { 0 }

    # Définition des labels pour l'itération courante

    $serviceLabels = @{

        service_name = $serviceName

        start_type   = $service.StartType.ToString()

        host         = $env:COMPUTERNAME

    }

    # Ajout de la valeur à la métrique

    $serviceMetric = $serviceMetric | Add-PrometheusMetricValue -Value $isUp -Labels $serviceLabels

}

# Envoi de la métrique contenant les données des trois services

$serviceMetric | Export-PrometheusPushgateway -GatewayUrl 'http://pushgateway.corp.local:9091' `

                              -Job 'service_monitoring'

Exposer les métriques en HTTP avec Pode

Si l’architecture de supervision nécessite que le script s’exécute comme un exporter classique interrogeable par Prometheus (modèle pull), la fonction Out-PrometheusMetric permet de récupérer les données formatées en texte brut.

Il suffit ensuite de lier ce flux à un module de serveur web dédié comme Pode.

Déléguer l’écoute HTTP à Pode permet de conserver un module de métriques strictement limité à la mise en forme des données, tout en laissant l’opportunité de s’appuyer sur les fonctionnalités d’un vrai serveur web (HTTPS, gestion des certificats, authentification, logs de requêtes) si le besoin s’en fait sentir.

L’exemple suivant expose la consommation de mémoire vive sur un endpoint /metrics :

Import-Module Pode

Import-Module PoshTorch

Start-PodeServer {

    # Listen on port 8080

    Add-PodeEndpoint -Address * -Port 8080 -Protocol Http

    # Define the /metrics route that Prometheus will scrape

    Add-PodeRoute -Method Get -Path '/metrics' -ScriptBlock {

        # Generate your metrics

        $metric = New-PrometheusMetric -Name 'ps_script_duration_seconds' `

                                    -Help 'Execution time of the backup script' `

                                    -Type gauge

        $metric | Add-PrometheusMetricValue -Value 45.2 -Labels @{ status = "success"; task = "db_backup" }

        $metric | Add-PrometheusMetricValue -Value 12.0 -Labels @{ status = "failed";  task = "log_cleanup" }

        # Génération du format texte Prometheus via StdOut

        $payload = $metric | Out-PrometheusMetric

# Renvoi de la réponse HTTP avec l'en-tête attendu par Prometheus

        Write-PodeTextResponse -Value $payload -ContentType 'text/plain; version=0.0.4'

    }

}

Au fait, vous vous demandez peut-être pourquoi le nom PoshTorch ? Direction le readme du repo Github pour la réponse !

AutoPilot: Post ESP – Installation des applications Win32 sous Intune

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
}

L’authentification échoue avec l’erreur « Size of the request headers is too long » sur Android

L’erreur “HTTP Error 400. The size of the request headers is too long” est relativement connue et plutôt bien documentée : elle correspond à l’envoi par une application client (par exemple Teams) d’un cookie (token) d’authentification trop long à son serveur Web, en général parce qu’il contient trop de claims par exemple lorsque l’utilisateur est membre de trop de groupes (phénomène dit de « token bloat »).

Il arrive cependant qu’elle survienne lors de l’authentification depuis un appareil spécifique, alors que tous les autres paramètres sont identiques (même compte, accès au même service dans le même tenant…).

On peut alors supposer que le token est corrompu : l’appareil en question conserve un cookie mal formé ou mal actualisé et tente de l’envoyer au serveur, qui rejette alors la demande.

Lorsque ce problème survient sur un téléphone Android, la première solution qui vient à l’esprit est la classique « suppression des données » de l’application qui échoue à se connecter, voire à désinstaller puis réinstaller cette application. Malheureusement cette manipulation peut ne pas fonctionner en raison d’un mécanisme méconnu : c’est en réalité le gestionnaire de comptes Android qui conserve les tokens d’authentification, pas directement les applications.

Il faut donc supprimer le compte dont l’authentification échoue via les paramètres « Comptes» d’Android pour supprimer le token problématique. La procédure varie légèrement d’un modèle de téléphone à un autre, elle ne sera donc pas détaillée ici, mais l’authentification devrait ensuite fonctionner comme au premier jour !