Add bash trap article

This commit is contained in:
Yorick Barbanneau 2022-07-05 00:23:16 +02:00
parent af9f9b65be
commit 9e3e4729ab
7 changed files with 601 additions and 0 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 darrê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 "<commande>" <list_signaux>
```
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.