diff --git a/content/articles/2022/bash_les_pieges/files/message.sh b/content/articles/2022/bash_les_pieges/files/message.sh new file mode 100644 index 0000000..b60e54d --- /dev/null +++ b/content/articles/2022/bash_les_pieges/files/message.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +DATE_FMT="+%s" + +msg() { + local message="$*" + [ -z "$message" ] && return + if [ -t 1 ] + then + printf "%b\n" "$message" + else + log "$message" + fi +} + +log() { + local message="${*-}" + [ -z "$message" ] && return + printf "%s %b\n" "$(date $DATE_FMT)" "$message" +} + +debug() { + local message="$*" + [[ -z $DEBUG || $DEBUG -ne 1 ]] && return + [ -z "$message" ] && return + message="DEBUG [${BASH_SOURCE[1]}:${FUNCNAME[1]}]: $message" + if [ -t 2 ] + then + >&2 msg "\e[34m$message\e[0m" + else + >&2 log "$message" + fi +} + +error() { + local message="$*" + [ -z "$message" ] && return + message="ERROR: $message" + if [[ -n $DEBUG && $DEBUG -eq 1 ]] + then + message="$message\n\tstack trace:\n" + for (( i=1; i<${#FUNCNAME[@]}; i++ )) + do + message="${message}\t source:${BASH_SOURCE[i]}" + message="${message} function:${FUNCNAME[$i]}" + message="${message} line:${BASH_LINENO[$i-1]}\n" + done + fi + if [ -t 2 ] + then + >&2 msg "\e[31m$message\e[0m" + else + >&2 log "$message" + fi +} diff --git a/content/articles/2022/bash_les_pieges/files/premier_script.sh b/content/articles/2022/bash_les_pieges/files/premier_script.sh new file mode 100755 index 0000000..edd9c87 --- /dev/null +++ b/content/articles/2022/bash_les_pieges/files/premier_script.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +server="192.168.0.254 -i ~/.ssh/rick.epha.se.ed25519" +user="ephase" + +# Reutilisons ce que nous avons créé lors du précédent article +source ./message.sh + +ssh_sock="/tmp/$(mktemp -u XXXXXXXXXX)" + +connect() { + msg "Create SSH main connection wih socket ${ssh_sock}" + ssh -N -o ControlMaster=yes -o ControlPath="$ssh_sock" -f ${user}@${server} || { + error "Can't connect to $server"; + exit 20; + } +} + +launch_command() { + if [ -z "$1" ] + then + error "Launch command require 1 parameter" + exit 31 + fi + local command + command="$1" + ssh -q -t -S "$ssh_sock" ${user}@${server} "$command" || { + error ""; + exit 30; + } +} + +cleanup() { + # close the master control connexion before + msg "Close SSH main connection" + ssh -q -o ControlPath="$ssh_sock" -O exit $server || { + error "Can't close SSH master connection, $ssh_sock remain"; + } +} +connect +trap cleanup EXIT +if [ "$1" = "error" ] +then + launch_command +fi +for (( i=1; i<=5; i++ )) +do + launch_command "echo 'Message N°$i from $server'" + sleep 5 +done diff --git a/content/articles/2022/bash_les_pieges/files/second_script.sh b/content/articles/2022/bash_les_pieges/files/second_script.sh new file mode 100755 index 0000000..8ab6299 --- /dev/null +++ b/content/articles/2022/bash_les_pieges/files/second_script.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +server="192.168.0.254 -i ~/.ssh/rick.epha.se.ed25519" +user="ephase" + +# Reutilisons ce que nous avons créé lors du précédent article +source ./message.sh + +ssh_sock="/tmp/$(mktemp -u XXXXXXXXXX)" + +connect() { + msg "Create SSH main connection wih socket ${ssh_sock}" + ssh -N -o ControlMaster=yes -o ControlPath="$ssh_sock" -f ${user}@${server} || { + error "Can't connect to $server"; + exit 20; + } +} + +launch_command() { + if [ -z "$1" ] + then + error "Launch command require 1 parameter" + exit 31 + fi + local command + command="$1" + ssh -q -t -S "$ssh_sock" ${user}@${server} "$command" || { + error ""; + exit 30; + } +} + +cleanup() { + # close the master control connexion before + msg "Close SSH main connection" + ssh -q -o ControlPath="$ssh_sock" -O exit $server || { + error "Can't close SSH master connection, $ssh_sock remain"; + } +} + +process_int() { + error "Script interrupted by user (SIGINT)" + exit 255 +} + +connect +trap cleanup EXIT +trap process_int INT +if [ "$1" = "error" ] +then + launch_command +fi +for (( i=1; i<=5; i++ )) +do + launch_command "echo 'Message N°$i from $server'" + sleep 5 +done diff --git a/content/articles/2022/bash_les_pieges/files/test_ssh.sh b/content/articles/2022/bash_les_pieges/files/test_ssh.sh new file mode 100755 index 0000000..8b3dd4e --- /dev/null +++ b/content/articles/2022/bash_les_pieges/files/test_ssh.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +server="192.168.0.254 -i ~/.ssh/rick.epha.se.ed25519" +user=ephase + +source message.sh + +ssh_sock="/tmp/$(mktemp -u XXXXXXXXXX)" + +connect() { + msg "Create SSH main connection wih socket ${ssh_sock}" + ssh -N -o ControlMaster=yes -o ControlPath="$ssh_sock" -f ${user}@${server} || { + # reuse + error "Can't connect to $server"; + exit 210; + } +} + +launch_command() { + local command + command="$1" + ssh -q -t -S "$ssh_sock" ${user}@${server} "$command" +} + +cleanup() { + # close the master control connexion before + msg "Close SSH main connection" + ssh -q -o ControlPath="$ssh_sock" -O exit ${server} || { + error "Can't close SSH master connection, $ssh_sock remain"; + exit 21; + } + exit +} +trap "cleanup 255" INT ERR +trap "cleanup" EXIT +connect +for (( i=10; i<=20; i++ )) +do + launch_command "echo 'Message N°$i from ${server}'" + sleep 1 +done diff --git a/content/articles/2022/bash_les_pieges/files/troisieme_script.sh b/content/articles/2022/bash_les_pieges/files/troisieme_script.sh new file mode 100755 index 0000000..4c1c88f --- /dev/null +++ b/content/articles/2022/bash_les_pieges/files/troisieme_script.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +server="192.168.0.254 -i ~/.ssh/rick.epha.se.ed25519" +user="ephase" + +# Reutilisons ce que nous avons créé lors du précédent article +source ./message.sh + +ssh_sock="/tmp/$(mktemp -u XXXXXXXXXX)" + +connect() { + msg "Create SSH main connection wih socket ${ssh_sock}" + ssh -N -o ControlMaster=yes -o ControlPath="$ssh_sock" -f ${user}@${server} || { + error "Can't connect to $server"; + exit 20; + } +} + +launch_command() { + if [ -z "$1" ] + then + error "Launch command require 1 parameter" + exit 31 + fi + local command + command="$1" + ssh -q -t -S "$ssh_sock" ${user}@${server} "$command" || { + error ""; + exit 30; + } +} + +cleanup() { + # close the master control connexion before + msg "Close SSH main connection" + ssh -q -o ControlPath="$ssh_sock" -O exit $server || { + error "Can't close SSH master connection, $ssh_sock remain"; + } +} + +process_int() { + error "Script interrupted by user (SIGINT)" + exit 255 +} + +check_conn() { + msg "Check connection on ${server}" + ssh -S "$ssh_sock" -O check $server + sleep 20 +} + +msg "Current PID: $$" +connect +trap cleanup EXIT +trap process_int INT +trap check_conn USR1 + +if [ "$1" = "error" ] +then + launch_command +fi +for (( i=1; i<=20; i++ )) +do + launch_command "echo 'Message N°$i from $server'" + sleep 2 +done diff --git a/content/articles/2022/bash_les_pieges/index.md b/content/articles/2022/bash_les_pieges/index.md new file mode 100644 index 0000000..760c2ad --- /dev/null +++ b/content/articles/2022/bash_les_pieges/index.md @@ -0,0 +1,336 @@ +Title: Bash avancé: les pièges (à signaux) +Category: sysadmin +Tags: bash, script, pl-fr +Date: 2022-11-15 8:30 +Cover: assets/backgrounds/ackbar-trap.jpg + +Pour l'instant, on ne peut pas dire que 2022 soit une année productive côté +article de blog. Le seul et unique article de cette année parlait de *Bash* qui +a eu un peu de succès. Comme toute les séries B un peu populaire, il lui fallait +une suite. Et bien la voici! + +Dans cette suite, il va être question de faire un peu le ménage lorsque votre +script s'arrête, que se soit gracieusement ou pas, mais pas que... + +## Dis, c'est quoi un signal + +Un signal est ue sorte de notification envoyée à un processus lorsqu'un +événement particulier a eu lieu. Plus concrètement, lorsque un programme ne +réponds pas dans votre terminal et que vous faites Ctrl+C, le système envoi le +signal SIGINT au processus en cours lui signifiant d'interrompre séance tenante +toute action. Chaque signal est associé à un numéro + +Voici un liste de quelques signaux (norme POSIX): + +Signal | Valeur | Action | Commentaire +--------|--------|--------|---------------------------------------------------- +SIGHUP | 1 | Term | Déconnexion du terminal ou fin du processus de contrôle +SIGINT | 2 | Term | Interruption depuis le clavier `CTRL + C` +SIGQUIT | 3 | Core | Demande ”Quitter” depuis le clavier `CTRL + \\` +SIGILL | 4 | Core | Instruction illégale +SIGABRT | 6 | Core | Signal d’arrêt depuis abort(3) +SIGFPE | 8 | Core | Erreur mathématique virgule flottante +SIGKILL | 9 | Term | Signal ”KILL” +... | ... | ... | ... + +## Les utiliser dans Bash : trap + +Paf, l'intrigue arrive comme ça d'un coup: nous allons utiliser la commande +`trap` pour gérer les signaux. La commande est plutôt simple à utiliser : + +```bash +trap "" +``` + +Attention cependant, tous les signaux ne peuvent pas être piégé: `SIGKILL` par +exemple ne peut pas donner lieu à exécution d'une commande. + +## Dans la vraie vie? + +Prenons un cas concret: un script nécessite l'ouverture d'une connexion SSH en +persistante à l'aide des options `ControlMaster` et `ContolPath` histoire de +pouvoir lancer plusieurs connexions successives plus rapidement (et sans se +ré-identifier). + +Si le script ne se passe pas comme prévu alors il est plutôt conseillé de faire +le ménage et terminer la connexion maitre. + +```bash +#!/usr/bin/env bash + +# Changer ces valeurs par celles adapté à votre configuration +server="192.168.0.254" +user="user" + +# Réutilisons ce que nous avons créé lors du précédent article +source ./message.sh + +ssh_sock="/tmp/$(mktemp -u XXXXXXXXXX)" + +connect() { + msg "Create SSH main connection wih socket ${ssh_sock}" + ssh -N -o ControlMaster=yes -o ControlPath="$ssh_sock" -f ${user}@${server} || { + error "Can't connect to $server"; + exit 20; + } +} + +launch_command() { + if [ -z "$1" ] + then + error "Launch command require 1 parameter" + exit 31 + fi + local command + command="$1" + ssh -q -t -S "$ssh_sock" ${user}@${server} "$command" || { + error "Error executing $command"; + exit 30; + } +} + +cleanup() { + msg "Close SSH main connection" + ssh -q -o ControlPath="$ssh_sock" -O exit $server || { + error "Can't close SSH master connection, $ssh_sock remain"; + } + # Sans ce exit, INT ne termine pas le script + exit +} + +connect + +trap cleanup EXIT + +if [ "$1" = "error" ] +then + launch_command +fi + +for (( i=1; i<=5; i++ )) +do + launch_command "echo 'Message N°$i from $server'" + sleep 1 +done +``` + +Ce script se lance sans paramètre ou avec le paramètre error afin de générer une +erreur. Étudions le un peu. + +La connexion principale est intiée par la fonction `connect`. Juste en dessous +de l'appel de cette dernière nous trouvons notre instruction `trap`. Elle +appelle la commande `cleanup` lorsque le signal `EXIT` est envoyé. + +`EXIT` n'est pas un signal standards su système mais interne à *Bash* comme +`ERR`, `DEBUG` ou `RETURN`. + +Ensuite, si l'argument `error` est passé à notre script, `launch_command` est +exécutée mais sans paramètre, ce qui ga générer une erreur. + +Enfin notre script exécute 5 fois en boucle la fonction `laucun_command` qui +se charge d'afficher un petit message sur la machine distante en utilisant la +connexion SSH créée dans notre fonction `connect`. + +Nous avons donc de quoi tester trois scénarios + +### Laisser le script finir normalement + +Appeler le script sans argument et le laisser se terminer sans intervenir est +notre premier scénario. Voici le résultat: + +```shell +$ ./script.sh +Create SSH main connection wih socket /tmp/TvCuLSzkMN +Enter passphrase for key '/home/user/.ssh/key.ed25519': +Message N°1 from 192.168.0.254 +Message N°2 from 192.168.0.254 +Message N°3 from 192.168.0.254 +Message N°4 from 192.168.0.254 +Message N°5 from 192.168.0.254 +Close SSH main connection +``` + +Nous pouvons voir que notre fonction `cleanup` a bien été appelée à la fin de +notre script exécutée par le signal `EXIT` + +### Générer une erreur + +Maintenant passons `error` en paramètre et observons le résultat: + +```shell +$ ./script.sh error +Enter passphrase for key '/home/user/.ssh/key.ed25519': +Create SSH main connection wih socket /tmp/Al77btSXKf +ERROR: Launch command require 1 parameter +Close SSH main connection +``` + +`launch_command` appelée sans paramètre conduit à la sortie de notre script avec +un code supérieur à 0. Cette sortie est capturée par `trap` qui lance aussi la +fonction de nettoyage. Vérifions le code de retour de notre script: + +```ssh +echo $? +31 +``` + +Le code de retour est bien celui défini dans notre fonction `launch_command` +lorsqu'on l'exécute sans paramètre. + +### Interrompre l'exécution + +Enfin observons ce qui se passe lorsque l'on interrompt l'exécution du script avec +Ctrl+C: + +```shell +$ ./script.sh +Create SSH main connection wih socket /tmp/0gFlBD6siZ +Enter passphrase for key '/home/user/.ssh/key.ed25519': +Message N°1 from 192.168.0.254 +Message N°2 from 192.168.0.254 +^CClose SSH main connection +``` + +Ici encore tout se passe comme prévu, sauf que le code de retour est 0, il +faudrait pouvoir changer se comportement afin de signifier que le script ne +s'est pas termine comme prévu. + +```shell +$ echo $? +0 +``` + +## Différencier les pièges en fonction du signal + +Tout est prévu dans *Bash* pour contourner ce problème: il est possible de +lancer plusieurs commandes `trap`. Dans notre script nous allons modifier un +petit peu notre script + +### Gérer le signal `SIGINT` + +Nous allons ajouter une fonction spécifique pour le signal juste après +`cleanup`: + +```bash +process_int(){ + error "Script interrupted by user (SIGINT)" + exit 255 +} +``` + +### ajouter un piège + +Maintenant nous n'avons plus qu'a ajouter un piège: + +```bash +# [...] +trap cleanup EXIT +trap process_int INT +# [...] +``` + +Et voilà, lors de l'exécution notre script et son interruption tout fonctionne +comme prévu: + +```shell +$ ./script.sh +Create SSH main connection wih socket /tmp/0gFlBD6siZ +Enter passphrase for key '/home/user/.ssh/key.ed25519': +Message N°1 from 192.168.0.254 +Message N°2 from 192.168.0.254 +^CERROR: Script interrupted by user (SIGINT) +Close SSH main connection + +$ echo $? +255 +``` + +## Vérifier la connexion au master avec un signal + +Il est possible d'utiliser tout un tas de signaux et de les intercepter avec +`trap`. Intéressons nous maintenant à `SIGUSR1`. C'est avec `SIGUSR2` des +signaux servant pour ce que l'on veut. Nous voulons afficher l'était de la +connexion SSH master. Rajoutons la fonction suivante après `launch_command`: + +```bash +# [...] +check_conn() { + msg "Check connection on ${server}" + ssh -S "$ssh_sock" -O stop $server + sleep 10 +} +#[...] +``` + +Puis de modifier le début de notre script comme ceci: + +```bash +# [...] +msg "Current PID: $$" +connect +trap cleanup EXIT +trap process_int INT +trap check_conn USR1 +# [...] +``` + +Au début nous affichons le PID de notre script, nous allons en avoir besoin pour +lui envoyer le signal `USR1` avec `kill` depuis un autre terminal. Et enfin +piégeons notre signal avec `trap` pour exécuter `check_conn`. + +Afin de disposer de plus de temps pour lancer la commande kill + +Afin d'avoir le temps de lancer la commande `kill`, augmentons de 20 le nombre +de tour de boucle effectué comme ci-dessous: + +```bash +# [...] + +for (( i=1; i<=20; i++ )) +do + launch_command "echo 'Message N°$i from $server'" + sleep 1 +done +``` + +### Lancer le script + +D'abord lançons le script et notons le numéros de PID: + +```shell +$ ./sscript.sh +Current PID: 9499 +Create SSH main connection wih socket /tmp/A4IdltbuxY +Enter passphrase for key '/home/user/.ssh/key.ed25519': +Message N°1 from 192.168.0.254 +``` +Puis dans un second terminal il nous suffit d'envoyer le signal: + +```shell +kill -USR1 9499 +``` + +Le signal est alors reçu par notre script qui va donc exécuter la fonction +attachée: + +```shell +[...] +Message N°6 from 192.168.0.254 +Message N°7 from 192.168.0.254 +Check connection on 192.168.0.254 +Master running (pid=9502) +``` + +Et continuer son exécution ensuite. + +## En conclusion + +Tout au long ce cet article, nous avons vu ce qu'était un signal et comment en +intercepter un (et même plusieurs) dans un script écrit en Bash. Bien entendu il +est possible d'utiliser les signaux dans d'autre langages, la façon de procéder +est similaire. + +Le nettoyage des traces laissées par un script qui ne se termine pas de la façon +attendue est la principale utilisation documentée ci et là dans différents +tutoriaux. Mais ce peut être un outils intéressant de communication +inter-processus dans vos scripts. diff --git a/content/assets/backgrounds/ackbar-trap.jpg b/content/assets/backgrounds/ackbar-trap.jpg new file mode 100644 index 0000000..ece101b Binary files /dev/null and b/content/assets/backgrounds/ackbar-trap.jpg differ