5 votes

Pourquoi un Shell script piégeant SIGTERM fonctionne-t-il lorsqu'il est exécuté manuellement, mais pas lorsqu'il est exécuté via launchd ?

Ok, tout simplement j'ai un shell script qui a besoin d'attendre que quelque chose se passe, mais il a un fichier de verrouillage et quelques processus enfants dont je dois m'assurer qu'ils sont rangés si le script est interrompu.

J'y suis parvenu sans problème en utilisant la fonction trap pour définir certaines actions appropriées, et j'ai créé un script qui ressemble un peu à ceci :

#!/bin/sh
LOG="$0.log"

# Create a lock-file to prevent simultaneous access
lockfile -l 86400 "$LOG.lock" || $(echo 'Locking failed' >&2 && exit 3)

# Create trap for interrupt and cleanup
on_complete() {
    echo $(date +%R)' Ended.' >> "$LOG"
    kill $(jobs -p)
    rm -f "$LOG.lock"
    exit
}
trap 'on_complete 2> /dev/null' SIGTERM SIGINT SIGHUP EXIT

# Do nothing
echo $(date +%R)' Running…' >> "$LOG"
sleep 86400 &
while wait; do sleep 86400 &; done

Cela peut être exécuté sans problème dans un terminal via sh Example.sh et en le terminant par Ctrl + C ce qui lui permet de retirer son fichier de verrouillage sans problème.

J'ai ensuite essayé de créer un launchd pour ce script comme ceci :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>org.example</string>
    <key>ProgramArguments</key>
    <array>
        <string>sh</string>
        <string>~/Downloads/Example.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>EnableGlobbing</key>
    <true/>
</dict>
</plist>

Création de Example.sh et Example.plist à partir de ce qui précède dans le fichier ~/Downloads me permet d'exécuter le launchd emploi via launchd load ~/Downloads/Example.plist et le terminer par launchd unload ~/Downloads/Example.plist . Cependant, la fin de l'emploi ne provoque pas un SIGTERM pour atteindre le script, qui est plutôt SIGKILL après le temps mort de 20 secondes.

Donc, ce que j'aimerais savoir, c'est pourquoi mon script ne reçoit pas SIGTERM et comment je peux m'en assurer ?

1 votes

Ce comportement se poursuit-il si vous supprimez 'sh' et appelez directement le script ? Je suppose que le script a son drapeau exécutable activé.

1 votes

Au lieu de 'launchd unload', avez-vous essayé 'launchctl stop ~/Downloads/Example.plist' ?

6voto

Joe Casadonte Points 1155

Le problème ultime ici est que Bash ne tue pas normalement ses enfants non-construits.

If bash is waiting for a command to complete and receives a signal for which a
trap has been set, the trap will not be executed until the command completes.
When bash is waiting for an asynchronous command  via  the  wait  builtin, the
reception of a signal for which a trap has been set will cause the wait builtin
to return immediately with an exit status greater than 128, immediately after
which  the trap is executed.

Quand vous frappez <CTRL>+<C> vous tuez le shell script, qui se comporte normalement -- mais le sleep continue. Utilisez ps à voir.

Quand on essaie d'arrêter les choses de l'extérieur, via kill puis Bash comme ci-dessus. Après une certaine période de temps (je suppose 20 secondes) launchd puis émet un kill -9 que le script ne peut pas piéger.

La solution consiste à émettre un wait après le sleep, pour indiquer à Bash qu'il peut s'interrompre :

sleep 86400 & wait

Cela permettra au script d'être interrompu, mais le sommeil survivra toujours. Je suis sûr qu'il y a un moyen de tuer les enfants, mais je n'ai pas pris la peine de chercher....

0 votes

Merci pour l'explication, mais l'utilisation de wait n'aide pas (c'est ce que je fais dans le vrai script que j'essaie de déboguer, j'ai modifié l'exemple pour qu'il corresponde un peu mieux), donc je ne suis pas sûr de ce qui se passe.

1 votes

Il s'avère qu'à partir de Yosemite, c'est maintenant la bonne réponse ; un agent de lancement ou un démon de lancement qui dort (de manière asynchrone) avec un piège approprié (doit être SIGINT pas INT ) recevront le signal avant d'être déchargés. Bien sûr, ce n'est pas bon pour Mavericks, Mountain Lion etc., mais c'est génial que cela fonctionne enfin comme il se doit, donc je pense que c'est la bonne réponse, mais il pourrait être utile d'ajouter que cela ne fonctionne correctement que sous 10.10.

4voto

ColonelMode Points 638

Je me rends compte que vous venez de partager avec nous un fragment de code et qu'il n'est pas évident de savoir ce que votre démon cherche à faire de plus que d'effectuer une action toutes les quelques secondes. Je vais donc faire quelques hypothèses sur la base de ce que vous avez écrit.

  1. On dirait que vous utilisez le fichier de verrouillage pour éviter les lancements en double.
  2. Il semble alors que vous ayez besoin du piège pour nettoyer le fichier de verrouillage utilisé pour mettre en œuvre votre test pour assurer la singularité.
  3. De plus, il semble que votre deamon fasse une boucle de sommeil pour se réveiller périodiquement et effectuer une action. (Juste dormir plus, dans votre exemple.)

Ce sont tous des problèmes que launchd est censé résoudre de manière plus efficace sous Darwin (et donc sous OS X).

Pour ce qui est de la ou des questions concernant le déchargement et le SIGTERM, plus précisément, lorsque vous unload votre launchdeamon reçoit un SIGKILL au lieu d'un SIGTERM. Si vous souhaitez simplement arrêter le travail ou lui envoyer un SIGTERM, utilisez alors stop au lieu de unload .

Si vous voulez qu'un SIGTERM soit envoyé sur unload vous devrez peut-être définir EnableTransactions . De même, si vous avez des tâches de nettoyage et que vous voulez que votre deamon reçoive des signaux pour le nettoyage et SIGTERM, vous devez configurer EnableTransactions comme partie du plist launchd pour votre script. <key>EnableTransactions</key><true/> . Ceci est décrit dans les documents à l'adresse https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html

Mais les trois mécanismes ci-dessus sont inutiles étant donné que le lancement...

Sous Darwin / OS X utilisant des démons de lancement, la méthode appropriée pour mettre en œuvre un démon de boucle de sommeil est d'utiliser StartInterval pour fonctionner sur un intervalle ou StartCalendarInterval pour exécuter la base à des moments précis. Utilisation de StartCalendarInterval donne en outre l'avantage que lorsque le système est endormi, il exécutera un intervalle de temps manqué au lieu de devoir attendre le prochain intervalle, et c'est généralement ce que vous voulez dans ces situations. Si vous avez un travail que vous voulez juste rester invoqué, pensez aussi à utiliser KeepAlive dans le cadre de la plist.

Il semble donc, d'après l'exemple de code que vous avez fourni, que vous voulez simplement exécuter quelque chose toutes les 86400 secondes. Si c'est le cas, launchd a un mécanisme pour le faire que vous devriez utiliser à la place et qui évite le besoin de votre fichier de verrouillage et de votre piège car launchd est conçu pour gérer tout cela pour vous automatiquement. Ce mécanisme est StartInterval et lorsqu'il est défini, il lancera votre deamon toutes les N secondes. Launchd s'assure également qu'il n'a pas lancé plusieurs copies de votre démon.

Ce mécanisme est décrit dans la documentation de launchd à l'adresse suivante https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html où il est dit :

StartInterval <integer>
This optional key causes the job to be started every N seconds.  If the system is
asleep, the job will be started the next time the computer wakes up.  If multiple
intervals transpire before the computer is woken, those events will be coalesced 
into one event upon wake from sleep.

Donc ton script darwinisé ~/Downloads/Example.sh regarderait quelque chose de très simple maintenant comme ça :

#!/bin/sh
echo $(date +%R)' Running…' # or whatever it is you wanted to do on the interval

Et votre plist ressemblerait à quelque chose comme ça :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>org.example</string>
    <key>ProgramArguments</key>
    <array>
        <string>sh</string>
        <string>~/Downloads/Example.sh</string>
    </array>
    <key>EnableGlobbing</key>
    <true/>
    <key>StartInterval</key>
    <integer>86400</integer>
    <key>StandardOutPath</key>
    <string>/mypathtolog/myjob.log</string>
    <key>StandardErrorPath</key>
    <string>/mypathtolog/myjob.log</string>
</dict>
</plist>

Notez que j'ai également ajusté cela pour définir les fichiers de journalisation ici d'une manière semblable à Darwin/launchd plutôt que dans le script lui-même. (Vous pourriez bien sûr les supprimer et les gérer dans votre script mais ce n'est pas nécessaire étant donné launchd).

Je note que vous pourriez également mettre en œuvre cette méthode en utilisant Program comme ça :

<key>Program</key>
<string>sh</string>
<key>ProgramArguments</key>
<array>
    <string>~/Downloads/Example.sh</string>
</array>

Vous pouvez également trouver http://launchd.info une référence utile ainsi que les docs d'Apple pour le fonctionnement de launchd à https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html

Des informations sur les démons exécutés périodiquement sont disponibles à l'adresse suivante https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html#//apple_ref/doc/uid/10000172i-CH1-SW2

1 votes

Désolé, j'aurais dû dire qu'en fait mon script attend la déconnexion/arrêt, ce qui est quand launchd enverra SIGINT à tous les agents en cours d'exécution (ou plutôt, devrait). EnableTransactions true ne fonctionnera pas car les shell scripts ne peuvent pas appeler les commandes vproc, et set to false est censé être la valeur par défaut. J'ai essayé de l'inclure comme false pour être sûr mais cela ne semble pas aider.

0 votes

OK, c'est une situation complètement différente de ce que votre script essayait apparemment de faire, et une dans laquelle la solution que vous avez élaborée est une solution complexe à un non-problème trivial. Il y a une meilleure façon d'attendre pour la déconnexion et l'arrêt dans OS X. Voir aussi EnableTransactions fonctionne pour les signaux en conséquence, que le processus utilise ou non vproc, le commentaire de la documentation est essentiellement destiné aux applications réelles, mais le fonctionnement est le même.

0 votes

Désolé, ça s'est sauvé avant que j'aie fini.

0voto

MikeBeaton Points 223

Vous ne devez pas définir EnableTransactions à moins que vous n'ayez l'intention d'appeler activement vproc_transaction_begin au démarrage (ce que vous ne pouvez pas faire directement à partir d'un shell script) suivi de vproc_transaction_end à l'arrêt ; activer ce paramètre et ensuite ne pas faire le premier appel marque votre script comme convenant à un arrêt soudain.

Vous souhaitez également mettre en œuvre la journalisation, car vous êtes assez limité dans ce que vous pouvez faire au niveau de l'interface utilisateur. launchd et vous devez voir ce qui échoue et pourquoi !

En les combinant, on obtient ceci /Library/LaunchDaemons/org.example.shutdownhook.plist (à cet endroit, il sera automatiquement lancé au démarrage) :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>org.example.shutdownhook</string>
    <key>ProgramArguments</key>
    <array>
        <string>sh</string>
        <string>/Library/PrivilegedHelperTools/org.example.shutdownhook.helper</string>
    </array>
    <key>StandardOutPath</key>
    <string>/var/log/org.example.shutdownhook.launchd/launchd.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/org.example.shutdownhook.launchd/launchd.log</string>
    <key>RunAtLoad</key>
    <true/>
    <key>EnableGlobbing</key>
    <false/>
    <key>EnableTransactions</key>
    <false/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

et ceci comme /Library/PrivilegedHelperTools/org.example.shutdownhook.helper :

#!/bin/sh

dolog() {
  echo "$(date) ${1}"
}

on_complete() {
  dolog "Trap ${1}"

  # do stuff
  # ...

  dolog "I did some stuff."

  dolog "Ended."

  # kill child (i.e. sleep) processes - launchd does this
  # automatically, but useful if running this script directly
  kill $(jobs -p)

  exit
}

# useful for debugging to see what signals you get;
# better to only trap specific signals in production
for s in {1..31} ;do trap "on_complete $s" $s ;done

while true
do
    dolog "Running…"
    sleep $(expr $RANDOM / 3277) & wait
done

Ce qui précède écrit toutes les 0-9 secondes dans le journal pour le débogage. Pour une utilisation sans débogage, utilisez sleep $RANDOM & wait (délai de 0 à 32767 secondes), et déplacer potentiellement déplacer dolog "Running…" au-dessus de la boucle while (selon que vous voulez ou non des pings périodiques vers le journal).

Vous pouvez utiliser sudo launchctl list | grep -v com.apple pour voir les sudo non-Apple launchd processus, y compris celui-ci, et vous pouvez tail -f le fichier journal pour voir ce qui se passe (par exemple, si vous arrêtez le processus, vous pouvez voir qu'il redémarre à cause de KeepAlive ).

REFS :
https://www.unix.com/man-page/osx/5/launchd.plist/
https://developer.apple.com/forums/thread/44221?answerId=622576022#622576022
https://stackoverflow.com/a/61909029/795690
https://apple.stackexchange.com/a/284652/113758

LesApples.com

LesApples est une communauté de Apple où vous pouvez résoudre vos problèmes et vos doutes. Vous pouvez consulter les questions des autres utilisateurs d'appareils Apple, poser vos propres questions ou résoudre celles des autres.

Powered by:

X