Que se passe-t-il si vous modifiez un script pendant l'exécution?

31

J'ai une question générale, qui pourrait être le résultat d'une mauvaise compréhension de la façon dont les processus sont gérés sous Linux.

Pour mes besoins, je vais définir un «script» comme un extrait de code bash enregistré dans un fichier texte avec les autorisations d'exécution activées pour l'utilisateur actuel.

J'ai une série de scripts qui s'appellent en tandem. Par souci de simplicité, je les appellerai les scripts A, B et C. Le script A exécute une série d'instructions puis s'arrête, il exécute ensuite le script B, puis il s'arrête, puis il exécute le script C. En d'autres termes, la série des étapes est quelque chose comme ceci:

Exécutez le script A:

  1. Série de déclarations
  2. Pause
  3. Exécuter le script B
  4. Pause
  5. Exécuter le script C

Je sais par expérience que si j'exécute le script A jusqu'à la première pause, puis que j'effectue des modifications dans le script B, ces modifications se reflètent dans l'exécution du code lorsque je l'autorise à reprendre. De même, si j'apporte des modifications au script C alors que le script A est toujours en pause, puis que je le laisse continuer après avoir enregistré les modifications, ces modifications se reflètent dans l'exécution du code.

Voici la vraie question alors, existe-t-il un moyen de modifier le script A alors qu'il est toujours en cours d'exécution? Ou l'édition est-elle impossible une fois son exécution commencée?

CaféineConnoisseur
la source
2
je pense que cela dépend de la coquille. bien que vous déclariez que vous utilisez bash. semble que cela dépend de la façon dont le shell charge les scripts en interne.
strugee
le comportement peut également changer si vous sourcez le fichier au lieu de l'exécuter.
strugee
1
Je pense que bash lit un script entier en mémoire avant de l'exécuter.
w4etwetewtwet
2
@handuel, non, ce n'est pas le cas. Comme s'il n'attendait pas jusqu'à ce que vous tapez "exit" à l'invite pour commencer à interpréter les commandes que vous avez entrées.
Stéphane Chazelas
1
@StephaneChazelas Oui, la lecture depuis le terminal ne le fait pas, mais c'est différent de l'exécution d'un script.
w4etwetewtwet

Réponses:

21

Sous Unix, la plupart des éditeurs travaillent en créant un nouveau fichier temporaire contenant le contenu modifié. Lorsque le fichier modifié est enregistré, le fichier d'origine est supprimé et le fichier temporaire renommé avec le nom d'origine. (Il existe, bien sûr, diverses protections pour empêcher la perte de données.) Il s'agit, par exemple, du style utilisé par sedou perllorsqu'il est invoqué avec le -idrapeau ("en place"), qui n'est pas vraiment "en place" du tout. Il aurait dû être appelé "nouvel endroit avec un ancien nom".

Cela fonctionne bien car unix garantit (au moins pour les systèmes de fichiers locaux) qu'un fichier ouvert continue d'exister jusqu'à sa fermeture, même s'il est "supprimé" et qu'un nouveau fichier portant le même nom est créé. (Ce n'est pas une coïncidence si l'appel système Unix pour "supprimer" un fichier est en fait appelé "délier".) Donc, de manière générale, si un interpréteur de shell a un fichier source ouvert et que vous "éditez" le fichier de la manière décrite ci-dessus , le shell ne verra même pas les modifications car il a toujours le fichier d'origine ouvert.

[Remarque: comme pour tous les commentaires normalisés, ce qui précède est sujet à de multiples interprétations et il existe différents cas d'angle, tels que NFS. Les pédants sont invités à remplir les commentaires avec des exceptions.]

Il est bien sûr possible de modifier directement des fichiers; ce n'est tout simplement pas très pratique à des fins d'édition, car même si vous pouvez écraser des données dans un fichier, vous ne pouvez pas supprimer ou insérer sans déplacer toutes les données suivantes, ce qui impliquerait beaucoup de réécriture. De plus, pendant que vous effectuiez ce déplacement, le contenu du fichier serait imprévisible et les processus qui avaient ouvert le fichier en souffriraient. Pour vous en sortir (comme avec les systèmes de base de données, par exemple), vous avez besoin d'un ensemble sophistiqué de protocoles de modification et de verrous distribués; des choses qui sont bien au-delà de la portée d'un utilitaire d'édition de fichiers typique.

Donc, si vous souhaitez éditer un fichier en cours de traitement par un shell, vous avez deux options:

  1. Vous pouvez ajouter au fichier. Cela devrait toujours fonctionner.

  2. Vous pouvez remplacer le fichier par un nouveau contenu exactement de la même longueur . Cela peut ou non fonctionner, selon que le shell a déjà lu cette partie du fichier ou non. Étant donné que la plupart des E / S de fichiers impliquent des tampons de lecture et que tous les shells que je connais lisent une commande composée entière avant de l'exécuter, il est peu probable que vous puissiez vous en sortir. Ce ne serait certainement pas fiable.

Je ne connais aucun libellé dans la norme Posix qui nécessite en fait la possibilité de l'ajouter à un fichier de script pendant que le fichier est en cours d'exécution, donc cela pourrait ne pas fonctionner avec chaque shell compatible Posix, et encore moins avec l'offre actuelle de presque- et des coques parfois compatibles posix. Alors YMMV. Mais pour autant que je sache, cela fonctionne de manière fiable avec bash.

Pour preuve, voici une implémentation "sans boucle" du fameux programme de 99 bouteilles de bière en bash, qui utilise ddpour écraser et ajouter (l'écrasement est probablement sûr car il remplace la ligne en cours d'exécution, qui est toujours la dernière ligne de la fichier, avec un commentaire exactement de la même longueur; je l'ai fait pour que le résultat final puisse être exécuté sans le comportement d'auto-modification.)

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #
rici
la source
Lorsque je lance cela, il commence par "Plus", puis continue à -1 et dans les nombres négatifs indéfiniment.
Daniel Hershcovich
Si je le fais export beer=100avant d'exécuter le script, cela fonctionne comme prévu.
Daniel Hershcovich
@DanielHershcovich: tout à fait raison; tests bâclés de ma part. Je pense que je l'ai réparé; il prend maintenant un paramètre de comptage facultatif. Une solution meilleure et plus intéressante serait de réinitialiser automatiquement si le paramètre ne correspond pas à la copie en cache.
rici
18

bash va un long chemin pour s'assurer qu'il lit les commandes juste avant de les exécuter.

Par exemple dans:

cmd1
cmd2

Le shell lira le script par blocs, il est donc probable qu'il lise les deux commandes, interprète la première, puis revienne à la fin du cmd1script et relise le script pour le lire cmd2et l'exécuter.

Vous pouvez facilement le vérifier:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(bien qu'en regardant la stracesortie à ce sujet, il semble que cela fasse des choses plus fantaisistes (comme lire les données plusieurs fois, chercher en arrière ...) que lorsque j'ai essayé la même chose il y a quelques années, donc ma déclaration ci-dessus à propos de la recherche de retour peut ne s'applique plus aux versions plus récentes).

Si toutefois vous écrivez votre script comme:

{
  cmd1
  cmd2
  exit
}

Le shell devra lire jusqu'à la fermeture }, le stocker en mémoire et l'exécuter. À cause de exit, le shell ne lira plus du script afin que vous puissiez le modifier en toute sécurité pendant que le shell l'interprète.

Sinon, lors de la modification du script, assurez-vous d'écrire une nouvelle copie du script. Le shell continuera de lire l'original (même s'il est supprimé ou renommé).

Pour ce faire, renommez- the-scriptle the-script.old, copiez- the-script.oldle the-scriptet modifiez-le.

Stéphane Chazelas
la source
4

Il n'y a vraiment aucun moyen sûr de modifier le script pendant son exécution car le shell peut utiliser la mise en mémoire tampon pour lire le fichier. De plus, si le script est modifié en le remplaçant par un nouveau fichier, les shells ne liront généralement le nouveau fichier qu'après avoir effectué certaines opérations.

Souvent, lorsqu'un script est modifié pendant son exécution, le shell finit par signaler des erreurs de syntaxe. Cela est dû au fait que, lorsque le shell ferme et rouvre le fichier de script, il utilise le décalage d'octet dans le fichier pour se repositionner au retour.

cendre
la source
4

Vous pouvez contourner ce problème en définissant un piège sur votre script, puis en utilisant execpour récupérer le nouveau contenu du script. Notez cependant que l' execappel démarre le script à partir de zéro et non à partir de l'endroit où il a atteint dans le processus en cours, et donc le script B sera appelé (et ainsi de suite).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

Cela continuera à afficher la date à l'écran. Je pourrais ensuite modifier mon script et passer dateà echo "Date: $(date)". En écrivant cela, le script en cours d'exécution affiche toujours la date. Cependant, si j'envoie le signal que j'ai défini trappour capturer, le script exec(remplace le processus en cours d'exécution par la commande spécifiée) qui est la commande $CMDet les arguments $@. Vous pouvez le faire en émettant kill -1 PID- où PID est le PID du script en cours d'exécution - et la sortie change pour s'afficher Date:avant la datesortie de la commande.

Vous pouvez stocker "l'état" de votre script dans un fichier externe (par exemple / tmp), et lire le contenu pour savoir où "reprendre" lorsque le programme sera réexécuté. Vous pouvez ensuite ajouter une terminaison d'interruptions supplémentaire (SIGINT / SIGQUIT / SIGKILL / SIGTERM) pour effacer ce fichier tmp. Ainsi, lorsque vous redémarrerez après avoir interrompu le "Script A", il recommencera depuis le début. Une version avec état serait quelque chose comme:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup
Drav Sloan
la source
J'ai résolu ce problème en capturant $0et $@au début du script et en utilisant ces variables à la execplace.
Drav Sloan