Évitez ces problèmes en limitant l'exécution des scripts Bash à une seule fois
Sommaire
Principaux points à retenir
- Assurez-vous qu'une seule instance de votre script est en cours d'exécution à l'aide de pgrep, lsof ou flock pour éviter les problèmes de concurrence.
- Implémentez facilement des contrôles pour mettre fin automatiquement à un script si d’autres instances en cours d’exécution sont détectées.
- En exploitant les commandes exec et env, la commande flock peut réaliser tout cela avec une seule ligne de code.
Certains scripts Linux ont une telle surcharge d'exécution qu'il faut éviter d'exécuter plusieurs instances à la fois. Heureusement, il existe plusieurs façons d'y parvenir dans vos propres scripts Bash.
Parfois une fois suffit
Certains scripts ne doivent pas être lancés si une instance précédente de ce script est toujours en cours d'exécution. Si votre script consomme trop de temps CPU et de RAM, ou génère beaucoup de bande passante réseau ou de surcharge de disque, il est logique de limiter son exécution à une seule instance à la fois.
Mais ce ne sont pas seulement les applications gourmandes en ressources qui doivent fonctionner de manière isolée. Si votre script modifie des fichiers, il peut y avoir un conflit entre deux (ou plusieurs) instances du script qui se disputent l'accès aux fichiers. Les mises à jour peuvent être perdues ou le fichier peut être corrompu.
Une technique pour éviter ces scénarios problématiques consiste à faire en sorte que le script vérifie qu'aucune autre version de lui-même n'est en cours d'exécution. S'il détecte d'autres copies en cours d'exécution, le script s'arrête automatiquement.
Une autre technique consiste à concevoir le script de telle manière qu'il se verrouille lorsqu'il est lancé, empêchant toute autre copie de s'exécuter.
Nous allons examiner deux exemples de la première technique, puis nous examinerons une façon de réaliser la seconde.
Utilisation de pgrep pour empêcher la concurrence
La commande pgrep recherche les processus en cours d'exécution sur un ordinateur Linux et renvoie l'ID de processus qui correspondent au modèle de recherche.
J'ai un script appelé loop.sh. Il contient une boucle for qui imprime l'itération de la boucle, puis se met en veille pendant une seconde. Il fait cela dix fois.
#!/bin/bashfor (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
J'en ai configuré deux instances en cours d'exécution, puis j'ai utilisé pgrep pour le rechercher par son nom.
pgrep loop.sh
Il localise les deux instances et indique leurs identifiants de processus. Nous pouvons ajouter l'option -c (count) pour que pgrep renvoie le nombre d'instances.
pregp -c loop.sh
Nous pouvons utiliser ce nombre d'instances dans notre script. Si la valeur renvoyée par pgrep est supérieure à un, il doit y avoir plusieurs instances en cours d'exécution et notre script se fermera.
Nous allons créer un script qui utilise cette technique. Nous l'appellerons pgrep-solo.sh.
La comparaison if teste si le nombre renvoyé par pgrep est supérieur à 1. Si c'est le cas, le script se termine.
if ( $(pgrep -c pgrep-solo.sh) -gt 1 ); then
echo "Another instance of $0 is running. Stopping."
exit 1
fi
Si le nombre retourné par pgrep est un, le script peut continuer. Voici le script complet.
#!/bin/bashecho "Starting."
if ( $(pgrep -c pgrep-solo.sh) -gt 1 ); then
echo "Another instance of $0 is running. Stopping."
exit 1
fi
for (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
Copiez ce fichier dans votre éditeur préféré et enregistrez-le sous le nom pgrep-solo.sh. Puis, rendez-le exécutable avec chmod.
chmod +x pgrep-loop.sh
Lorsqu'il fonctionne, il ressemble à ceci.
./pgrep-solo.sh
Mais si j'essaie de le démarrer avec une autre copie déjà en cours d'exécution dans une autre fenêtre de terminal, il le détecte et quitte.
./pgrep-solo.sh
Utilisation de lsof pour empêcher la concurrence
Nous pouvons faire une chose très similaire avec la commande lsof.
Si nous ajoutons l'option -t (constante), lsof répertorie les ID de processus.
lsof -t loop.sh
Nous pouvons canaliser la sortie de lsof vers wc. L'option -l (lignes) compte le nombre de lignes qui, dans ce scénario, est le même que le nombre d'ID de processus.
lsof -t loop.sh | wc -l
Nous pouvons utiliser cela comme base du test dans la comparaison if dans notre script.
Enregistrez cette version sous le nom lsof-solo.sh.
#!/bin/bashecho "Starting."
if ( $(lsof -t "$0" | wc -l) -gt 1 ); then
echo "Another instance of $0 is running. Stopping."
exit 1
fi
for (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
Utilisez chmod pour le rendre exécutable.
chmod +x lsof-solo.sh
Maintenant, avec le script lsof-solo.sh exécuté dans une autre fenêtre de terminal, nous ne pouvons pas démarrer une deuxième copie.
./lsof-solo.sh
La méthode pgrep ne nécessite qu'un seul appel à un programme externe (pgrep), la méthode lsof en nécessite deux (lsof et wc). Mais l'avantage de la méthode lsof par rapport à la méthode pgrep est que vous pouvez utiliser la variable $0 dans la comparaison if. Celle-ci contient le nom du script.
Cela signifie que vous pouvez renommer le script et qu'il fonctionnera toujours. Vous n'avez pas besoin de penser à modifier la ligne de comparaison if et à insérer le nouveau nom du script. La variable $0 inclut le './' au début du nom du script (comme ./lsof-solo.sh), et pgrep ne l'aime pas.
Utilisation de Flock pour empêcher la concurrence
Notre troisième technique utilise la commande flock, qui est conçue pour définir des verrous de fichiers et de répertoires à partir de scripts. Tant qu'il est verrouillé, aucun autre processus ne peut accéder à la ressource verrouillée.
Cette méthode nécessite l'ajout d'une seule ligne en haut de votre script.
( "${GEEKLOCK}" != "$0" ) && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :
Nous allons bientôt décoder ces hiéroglyphes. Pour l'instant, vérifions simplement que cela fonctionne. Enregistrez celui-ci sous le nom flock-solo.sh.
#!/bin/bash( "${GEEKLOCK}" != "$0" ) && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :
echo "Starting."
for (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
Bien sûr, nous devons le rendre exécutable.
chmod +x flock-solo.sh
J'ai démarré le script dans une fenêtre de terminal, puis j'ai essayé de l'exécuter à nouveau dans une autre fenêtre de terminal.
./flock-solo
./flock-solo
./flock-solo
Je ne peux pas lancer le script tant que l'instance dans l'autre fenêtre de terminal n'est pas terminée.
Décryptons la ligne qui fait la magie. Au cœur de celle-ci se trouve le commandement du troupeau.
flock -en "$0" "$0" "$@"
La commande flock est utilisée pour verrouiller un fichier ou un répertoire, puis pour exécuter une commande. Les options que nous utilisons sont -e (exclusif) et -n (non bloquant).
L'option exclusive signifie que si nous réussissons à verrouiller le fichier, personne d'autre ne peut y accéder. L'option non bloquante signifie que si nous ne parvenons pas à obtenir un verrou, nous arrêtons immédiatement d'essayer. Nous ne réessayons pas pendant un certain temps, nous nous retirons immédiatement avec grâce.
Le premier $0 indique le fichier que nous souhaitons verrouiller. Cette variable contient le nom du script en cours.
Le deuxième $0 est la commande que nous voulons exécuter si nous réussissons à obtenir un verrou. Encore une fois, nous transmettons le nom de ce script. Parce que le verrou bloque tout le monde à part de nous, nous pouvons lancer le fichier script.
Nous pouvons transmettre des paramètres à la commande qui est lancée. Nous utilisons $@ pour transmettre tous les paramètres de ligne de commande qui ont été transmis à ce script, à la nouvelle invocation du script qui va être lancé par flock.
Nous avons donc ce script qui verrouille le fichier de script, puis lance une autre instance de lui-même. C'est presque ce que nous voulons, mais il y a un problème. Lorsque la deuxième instance est terminée, le premier script reprend son traitement. Cependant, nous avons un autre tour dans notre sac pour y faire face, comme vous le verrez.
Nous utilisons une variable d'environnement que nous appelons GEEKLOCK pour indiquer si un script en cours d'exécution doit appliquer le verrou ou non. Si le script a été lancé et qu'aucun verrou n'est en place, le verrou doit être appliqué. Si le script a été lancé et qu'un verrou est en place, il n'a rien à faire, il peut simplement s'exécuter. Avec un script en cours d'exécution et le verrou en place, aucune autre instance du script ne peut être lancée.
( "${GEEKLOCK}" != "$0" )
Ce test se traduit par « renvoyer vrai si la variable d'environnement GEEKLOCK est pas défini sur le nom du script.' Le test est enchaîné au reste de la commande par && (et) et || (ou). La partie && est exécutée si le test renvoie vrai, et la section || est exécutée si le test renvoie faux.
env GEEKLOCK="$0"
La commande env est utilisée pour exécuter d'autres commandes dans des environnements modifiés. Nous modifions notre environnement en créant la variable d'environnement GEEKLOCK et en la définissant sur le nom du script. La commande que la commande env va lancer est la commande flock, et la commande flock lance la deuxième instance du script.
La deuxième instance du script effectue sa vérification pour voir si la variable d'environnement GEEKLOCK n'a pas existe, mais constate qu'il existe. La section || de la commande est exécutée, qui ne contient rien d'autre que deux points ':' qui est en fait une commande qui ne fait rien. Le chemin d'exécution parcourt ensuite le reste du script.
Mais nous avons toujours le problème du premier script qui poursuit son propre traitement lorsque le second script est terminé. La solution à ce problème est la commande exec. Celle-ci exécute d'autres commandes en remplaçant le processus appelant par le processus nouvellement lancé.
exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@"
Donc la séquence complète est :
- Le script se lance et ne trouve pas la variable d'environnement. La clause && est exécutée.
- exec lance env et remplace le processus de script d'origine par le nouveau processus env.
- Le processus env crée la variable d'environnement et lance flock.
- flock verrouille le fichier script et lance une nouvelle instance du script qui détecte la variable d'environnement, exécute la clause || et le script peut s'exécuter jusqu'à sa conclusion.
- Étant donné que le script d'origine a été remplacé par le processus env, il n'est plus présent et ne peut pas continuer son exécution lorsque le deuxième script se termine.
- Étant donné que le fichier de script est verrouillé lorsqu'il est en cours d'exécution, les autres instances ne peuvent pas être lancées tant que le script lancé par flock n'arrête pas de s'exécuter et ne libère pas le verrou.
Cela pourrait ressembler à l'intrigue d'Inception, mais ça fonctionne à merveille. Cette réplique est vraiment percutante.
Pour plus de clarté, c'est le verrou sur le fichier de script qui empêche le lancement d'autres instances, et non la détection de la variable d'environnement. La variable d'environnement indique uniquement à un en cours d'exécution script pour définir le verrou, ou que le verrou est déjà en place.
Verrouiller et charger
Il est plus facile que vous ne le pensez de garantir qu'une seule instance d'un script est exécutée à la fois. Ces trois techniques fonctionnent. Bien qu'elle soit la plus compliquée à utiliser, la ligne unique de flock est la plus facile à insérer dans n'importe quel script.