PowerShell : Intégrer un timeout à la boucle While
I. Présentation
On se retrouve dans ce tutoriel pour parler PowerShell et timeout afin de pouvoir optimiser vos scripts notamment dans une boucle While. Certaines fois, nous avons besoin d'attendre qu'une action se termine avant d'en commencer une autre, par exemple attendre le changement d'état d'un service, d'un processus ou encore la fin de l'exécution d'une tâche planifiée.
Je vais vous présenter la classe .NET "Stopwatch" puisqu'elle permet de répondre à ce besoin de timeout grâce à sa fonction de timer. Vous comprendrez rapidement que cette classe peut aussi être utilisée pour d'autres choses, comme calculer le temps d'exécution d'un script, voire d'une section d'un script... Intéressant donc aussi pour le debug.
En savoir plus sur les boucles Do-While et Do-Until en PowerShell
II. La Classe Stopwatch
Avant de parler timeout, on va déjà commencer par apprendre quelques commandes pour utiliser la classe Stopwatch simplement. Pour ceux qui voudront aller plus loin, rendez-vous sur l'aide : Help Stopwatch
La classe Stopwatch appartient à l'espace de noms System.Diagnostics, on pourra donc créer et démarrer un nouveau timer avec la commande suivante (avec l'affectation dans une variable qui nous sera utile ensuite) via la méthode StartNew().
$timer = [Diagnostics.Stopwatch]::StartNew()
Désormais le compteur va tourner... Les secondes s'écoulent... Pour voir où en est le compteur, on va afficher la propriété "Elapsed.TotalSeconds" ou "Elapsed.Seconds" selon la précision du format que l'on souhaite. Voici les commandes associées ainsi qu'une copie d'écran qui vous permettra de bien comprendre la différence entre les deux propriétés :
$timer.Elapsed.TotalSeconds $timer.Elapsed.Seconds
Ce qui donne :
Pour arrêter le timer, on utilise la méthode Stop() :
$timer.Stop()
Ceci peut faire aussi de pause, car si vous invoquez la méthode Start() alors le compteur repartira en reprenant où il c'était arrêté. Par contre, si vous invoquez la méthode Reset() ou si vous recréez un timer dans la même variable ($timer) il va repartir de zéro.
$timer.Start()
Vous avez maintenant les bases d'utilisation de la classe Stopwatch, finissons tout de même avec cette boucle for qui va permettre de voir que le timer tourne bien :
# Démarrer le timer/chrono $timer = [Diagnostics.Stopwatch]::StartNew() # Boucle d'exemple affichage timer for ($i = 1; $i -lt 3000; $i++) { # Secondes avec les millisecondes Write-Host $timer.Elapsed.TotalSeconds } # Arrêter le timer $timer.Stop()
Avec une boucle While ça marche aussi, pour afficher le compteur tant qu'il est inférieur à 5 secondes :
# Démarrer le timer/chrono $timer = [Diagnostics.Stopwatch]::StartNew() While($timer.Elapsed.Seconds -lt 5){ Write-Host $timer.Elapsed.TotalSeconds } # Arrêter le timer $timer.Stop()
Passons maintenant à notre timeout...
III. Timeout avec la classe Stopwatch
Finalement, on va reprendre l'idée de la boucle While décrite ci-dessus, où la valeur "5" représente la valeur timeout. Il suffirait de définir une variable nommée - par exemple - $timeout, de lui attribuer une valeur en secondes ça fait un timeout dans la boucle While. Par contre, c'est plus intéressant de le coupler avec une autre action, car le but d'une timeout est de sécuriser le code pour éviter qu'une boucle tourne à l'infini, en temps normal, ce n'est pas lui qui doit permettre de quitter la boucle.
Prenons l'exemple d'un bout de code où l'on attends qu'un service passe sur l'état "Stopped" pour continuer... Si cela n'arrive jamais, la boucle va tourner à l'infini ce qui n'est pas acceptable. On va donc s'appuyer sur ce que l'on a vu précédemment pour ajouter un timeout.
Tout va se jouer dans la boucle While, où l'on va dire "Tant que le statut du service DHCP n'est pas sur Stopped et que le timer est plus petit que le timeout, alors fait cette action...". Ce qui signifie que dès que le service passera sur l'état Stopped on quittera la boucle mais si cela n'arrive jamais, c'est le fait que le timer dépasse la valeur timeout qui nous fera quitter la boucle. La condition ET (and) est très importante dans le cadre de l'utilisation d'un timeout pour que la boucle puisse continuer seulement si les deux conditions sont vraies.
Par ailleurs, dans l'action du While, je conseille de déclencher des pauses à chaque fois (à vous de voir pour le nombre de secondes selon ce que vous attendez comme action) ça évitera de faire trop de tour de boucle inutilement.
Voici le script en question que je vous invite à exécuter sur votre machine :
# Timeout en secondes $timeout = 10 # Démarrer le timer/chrono $timer = [Diagnostics.Stopwatch]::StartNew() # Utiliser le timer pour créer un timeout sur un While While (((Get-Service DHCP).Status -ne "Stopped") -and ($timer.Elapsed.TotalSeconds -lt $timeout)) { Write-Host "[$($timer.Elapsed.Seconds)] En attente de l'arret du service Client DHCP..." Start-Sleep -Seconds 3 } # Arrêter le timer $timer.Stop()
Ce qui nous donne :
Le fait d'intégrer un timeout implique que le script va continuer alors qu'une action n'a pas réussie, ce qui peut engendrer d'autres problèmes dans l'exécution de votre script. L'idée peut-être de vérifier dans une condition "if" si le timer est supérieur au timeout, et si c'est le cas de terminer le script en affichant un message d'erreur. La nécessité ou non de faire cette vérification dépend de l'objectif de votre script, mais je préfère vous mettre la puce à l'oreille.
Note : L'exemple des tâches planifiées ou des services est plus pertinent que de faire le même exemple avec un processus, car il existe un commandlet "Wait-Process" qui permet d'attendre la fin d'un processus, et il contient un paramètre timeout directement.
Pour attendre la fin d'exécution d'une tâche planifiée c'est-à-dire qu'elle retrouve l'état "Prêt" (Ready), on peut utiliser la condition While suivante :
While ((Get-ScheduledTask -TaskName "MaTache").State -ne "Ready") -and ($timer.Elapsed.TotalSeconds -lt $timeout))
J'en ai terminé avec les explications sur la gestion d'un timeout en PowerShell au sein d'une boucle While, je vous encourage à intégrer ce type de fonctionnement à vos scripts pour éviter les boucles infinies. Et puis n'hésitez pas à utiliser aussi la classe Stopwatch pour d'autres usages. 🙂
Bonjour,
Donc si je comprends bien, je peux utiliser ton bout de code pour ma problématique :
J’ai un script qui doit stopper un service, mais le service parfois ne répond pas du coup le script reste bloqué … Donc je place un timeout, et si après 3 tentatives, le service ne se stoppe pas, le script peut continuer et tuer le processus du service?
Cordialement,
Florian
Bonsoir,
Quand le script reste bloqué, il reste bloqué sur la commande Stop-Service ? Sinon oui le timeout en faisant bien la condition peut te sortir de ce blocage 😉
Florian