¿Por qué un script de shell que captura SIGTERM funciona cuando se ejecuta manualmente, pero no cuando se ejecuta a través de launchd?

De acuerdo, simplemente tengo un script de shell que necesita esperar a que suceda algo, pero tiene un archivo de bloqueo y algunos procesos secundarios que debo asegurarme de que estén ordenados si se interrumpe el script.

Logré esto sin problemas al usar el trapcomando para establecer algunas acciones apropiadas y se me ocurrió un script que se parece un poco a esto:

#!/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

Esto se puede ejecutar muy bien en una terminal a través de sh Example.sh, y terminarlo con Ctrl + C, lo que hace que elimine su archivo de bloqueo sin ningún problema.

Luego intenté crear un launchdtrabajo para este script así:

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

La creación de Example.sh y Example.plist a partir de lo anterior en la ~/Downloadscarpeta me permite ejecutar el launchdtrabajo a través de launchd load ~/Downloads/Example.plisty finalizarlo a través de launchd unload ~/Downloads/Example.plist. Sin embargo, finalizar el trabajo no hace que a SIGTERMllegue al script, que en su lugar se SIGKILLejecuta después del tiempo de espera de 20 segundos.

Entonces, lo que me gustaría saber es; ¿Por qué mi secuencia de comandos no recibe SIGTERMy cómo puedo asegurarme de que lo haga?

¿Continúa este comportamiento si elimina 'sh' y llama directamente al script? Supongo que el script tiene establecido su indicador ejecutable.
En lugar de 'launchd unload', ¿ha probado 'launchctl stop ~/Downloads/Example.plist'?

Respuestas (3)

El último problema aquí es que Bash normalmente no mata a sus hijos no incorporados.

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.

Cuando golpeas, <CTRL>+<C>estás matando el script de shell, que se comporta normalmente, pero el sueño sigue vivo. Uso pspara ver.

Cuando intente detener las cosas externamente, a través de kill, luego Bash como se indicó anteriormente. Después de un período de tiempo de espera (supongo que 20 segundos) launchd, emite un mensaje kill -9que el script no puede atrapar.

La solución es emitir una espera después del sueño, para indicarle a Bash que puede interrumpirse a sí mismo:

sleep 86400 & wait

Esto permitirá que se interrumpa el guión, pero el sueño aún sobrevivirá. Estoy seguro de que hay una forma de matar a los niños, pero no me molesté en buscarla...

Gracias por la explicación, pero el uso de waitno ayuda (es lo que estoy haciendo en el script real que estoy tratando de depurar, he modificado el ejemplo para que coincida un poco más), así que no estoy seguro qué está sucediendo.
Resulta que a partir de Yosemite esta es ahora la respuesta correcta; un agente de lanzamiento o un demonio de lanzamiento que esté durmiendo (asincrónicamente) con una trampa adecuada (debe ser SIGINT, no INT) recibirá la señal antes de descargarse. Por supuesto, esto no es bueno para Mavericks, Mountain Lion, etc., pero es genial que finalmente funcione como debería, así que estoy marcando que esta es la respuesta correcta, pero podría valer la pena editarlo porque solo funciona correctamente en 10.10 .

Al darse cuenta de que acaba de compartir con nosotros un fragmento de código esencialmente y no está claro qué más está buscando lograr su daemon además de realizar alguna acción cada tantos segundos. Así que voy a hacer algunas suposiciones basándome en lo que has escrito.

  1. Parece que está utilizando el archivo de bloqueo para evitar un inicio duplicado.
  2. Entonces parece que necesita la trampa para limpiar el archivo de bloqueo utilizado para implementar su prueba para asegurar la singularidad.
  3. Además, parece que tu demonio está haciendo un ciclo de sueño para despertarse periódicamente y realizar alguna acción. (Solo duerme más, en tu ejemplo).

Todos estos son problemas que launchd está destinado a resolver de una mejor manera en Darwin (y, por lo tanto, en OS X).

En cuanto a la(s) pregunta(s) con la descarga y SIGTERM, específicamente, cuando se le unloadenvía a su launchdeamon un SIGKILL en lugar de un SIGTERM. Si solo desea detener el trabajo o enviarle un SIGTERM, use stopen lugar de unload.

Si desea que se envíe un SIGTERM, unloades posible que deba configurar EnableTransactions. Del mismo modo, si tiene tareas de limpieza y desea que su deamon reciba señales para la limpieza y SIGTERM, debe configurarlo EnableTransactionscomo parte de la lista de inicio para su secuencia de comandos. <key>EnableTransactions</key><true/>. Esto se describe en los documentos en https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html

Pero los tres mecanismos anteriores son innecesarios dado el lanzamiento...

Bajo Darwin / OS X usando launchdaemons, el método apropiado para implementar un demonio de bucle de suspensión es usarlo StartIntervalpara ejecutarse en un intervalo o StartCalendarIntervalpara ejecutarse en momentos específicos. El uso StartCalendarIntervaladicional brinda la ventaja de que cuando el sistema está dormido, ejecutará un intervalo de tiempo perdido en lugar de tener que esperar al siguiente intervalo, y generalmente es lo que desea en estas situaciones. Si tiene un trabajo que solo desea seguir invocando, también considere usarlo KeepAlivecomo parte de la plist.

Entonces parece que, según el ejemplo de código que proporcionó, solo desea ejecutar algo cada 86400 segundos. Si este es el caso, launchd tiene un mecanismo para hacer esto que debería usar en su lugar y evita la necesidad de su archivo de bloqueo y trampa por completo, ya que launchd está diseñado para manejar todo esto automáticamente. Ese mecanismo es StartIntervaly, cuando se define, lanzará su deamon cada N segundos. Launchd también se asegura de que no haya lanzado múltiples copias de su daemon.

Este mecanismo se describe en los documentos de launchd en https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html donde dice:

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.

Entonces, su script darwinizado ~/Downloads/Example.shse vería muy simple ahora como esto:

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

Y tu plist se vería así:

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

Tenga en cuenta que también ajusté esto para configurar los archivos de registro aquí de una manera similar a Darwin/launchd en lugar de en el script en sí. (Por supuesto, podría eliminarlos y manejarlos en su secuencia de comandos, pero no es necesario dado el lanzamiento).

Me gustaría señalar que también podría implementar esto usando Programasí:

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

También puede encontrar http://launchd.info una referencia útil junto con los documentos de Apple sobre cómo funciona launchd en https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ Introducción.html

Puede encontrar información sobre los demonios que se ejecutan periódicamente en https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html#//apple_ref/doc/uid/10000172i-CH1-SW2

Lo siento, debería haber dicho que en realidad mi secuencia de comandos está esperando el cierre de sesión/apagado, que es cuando launchdse enviará SIGINTa todos los agentes en ejecución (o más bien, debería). EnableTransactionstrue no funcionará, ya que los scripts de shell no pueden llamar a los comandos vproc, y se supone que el valor predeterminado es false. Intenté incluirlo como falso para estar seguro, pero no parece ayudar.
De acuerdo, esa es una situación completamente diferente de lo que aparentemente intentaba hacer su secuencia de comandos, y una en la que la solución que ha creado es una solución compleja para un problema trivial. Hay una manera completamente mejor de esperar el cierre de sesión y el apagado en OS X. También EnableTransactionsfunciona para señales en consecuencia, independientemente de si el proceso realmente usa vproc o no, ese comentario de documentación es básicamente para aplicaciones reales, pero es funcionalmente igual independientemente.
Lo siento, eso se guardó antes de que terminara.
Puede crear un LaunchAgent en espera de cierre de sesión o utilizar un gancho de cierre de sesión ( sudo defaults write com.apple.loginwindow LogoutHook /Users/Shared/logoutHook.sh).
¿El gancho de cierre de sesión requiere sudo o se puede hacer por usuario? La forma correcta de ver si se cierra la sesión o no, todavía no sé por qué no estoy entrando SIGINTen mi script tal como está; ¿pudiste reproducir el problema ejecutando el código de ejemplo que proporcioné? Ninguno de los ajustes EnableTransactionsparece hacer una diferencia para mí.
Además, la documentación del desarrollador de Apple en realidad desaconseja el uso de ganchos de inicio/cierre de sesión. Entonces, por lo que puedo decir, usar un script dormido es, de hecho, la forma "correcta" de hacer esto para un agente de lanzamiento, además, hay otros casos, como trabajos de ejecución prolongada, donde la captura sigue siendo SIGINTimportante.

No debe configurar EnableTransactionsa menos que vaya a llamar activamente vproc_transaction_beginal inicio (lo que no puede hacer directamente desde un script de shell) seguido de vproc_transaction_endal apagar; habilitar esta configuración y luego no realizar la primera llamada marca su secuencia de comandos como adecuada para una terminación repentina.

También desea implementar el registro, ya que está bastante limitado en lo que puede hacer al launchdapagar, ¡y necesita ver qué está fallando y por qué!

Combinando estos, obtienes esto como /Library/LaunchDaemons/org.example.shutdownhook.plist(en esta ubicación, se iniciará automáticamente en el arranque):

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

y esto como /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

Lo anterior escribe cada 0-9 segundos en el registro para la depuración. Para un uso que no sea de depuración, use sleep $RANDOM & wait(0-32767 segundos de retraso) y muévase potencialmente dolog "Running…"por encima del ciclo while (dependiendo de si desea pings periódicos al registro).

Puede usar sudo launchctl list | grep -v com.applepara ver procesos sudo que no son de Apple, launchdincluido este, y puede usar tail -fel archivo de registro para ver qué está pasando (por ejemplo, si elimina el proceso, puede verlo reiniciar debido a 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