Warum funktioniert ein Shell-Skript, das SIGTERM abfängt, wenn es manuell ausgeführt wird, aber nicht, wenn es über launchd ausgeführt wird?

Okay, ganz einfach, ich habe ein Shell-Skript, das warten muss, bis etwas passiert, aber es hat eine Sperrdatei und einige untergeordnete Prozesse, die ich sicherstellen muss, dass sie aufgeräumt werden, wenn das Skript unterbrochen wird.

Ich habe dies ohne Probleme erreicht, indem ich mit dem trapBefehl einige geeignete Aktionen festgelegt habe, und habe ein Skript entwickelt, das ungefähr so ​​​​aussieht:

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

Dies kann problemlos in einem Terminal über ausgeführt sh Example.shund mit beendet werden Ctrl + C, wodurch die Sperrdatei ohne großen Aufwand entfernt wird.

Ich habe dann versucht, einen launchdJob für dieses Skript wie folgt zu erstellen:

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

Durch das Erstellen von Example.sh und Example.plist aus dem obigen Ordner im Ordner kann ich den Job über ~/Downloadsausführen und über beenden . Das Beenden des Jobs führt jedoch nicht dazu, dass a das Skript erreicht, das stattdessen nach dem 20-Sekunden-Timeout ausgeführt wird.launchdlaunchd load ~/Downloads/Example.plistlaunchd unload ~/Downloads/Example.plistSIGTERMSIGKILL

Was ich also wissen möchte, ist; Warum empfängt mein Skript nicht SIGTERMund wie kann ich sicherstellen, dass dies der Fall ist?

Bleibt dieses Verhalten bestehen, wenn Sie „sh“ entfernen und das Skript direkt aufrufen? Ich gehe davon aus, dass das Skript sein ausführbares Flag gesetzt hat.
Haben Sie statt 'launchd unload' 'launchctl stop ~/Downloads/Example.plist' versucht?

Antworten (3)

Das ultimative Problem dabei ist, dass Bash seine nicht eingebauten Kinder normalerweise nicht tötet.

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.

Wenn du drückst, <CTRL>+<C>tötest du das Shell-Skript, das sich normal verhält – aber der Schlaf lebt weiter. Verwenden Sie ps, um zu sehen.

Wenn Sie versuchen, die Dinge extern zu stoppen, über kill, dann Bash wie oben. Nach einer gewissen Zeitüberschreitung (ich schätze 20 Sekunden) launchdwird dann ein ausgegeben, kill -9das das Skript nicht abfangen kann.

Die Lösung besteht darin, nach dem Ruhezustand ein Wait auszugeben, um Bash anzuzeigen, dass es sich selbst unterbrechen kann:

sleep 86400 & wait

Dadurch kann das Skript unterbrochen werden, aber der Ruhezustand bleibt bestehen. Ich bin mir sicher, dass es einen Weg gibt, die Kinder zu töten, aber ich habe mich nicht darum gekümmert, danach zu suchen ...

Danke für die Erklärung, aber die Verwendung von waithilft nicht (das mache ich in dem eigentlichen Skript, das ich zu debuggen versuche, ich habe das Beispiel so angepasst, dass es etwas genauer übereinstimmt), also bin ich mir nicht sicher was ist los.
Es stellt sich heraus, dass dies ab Yosemite jetzt die richtige Antwort ist; Ein Launch-Agent oder Launch-Daemon, der (asynchron) mit einem geeigneten Trap (muss SIGINT, not INT) schläft, empfängt das Signal, bevor er entladen wird. Natürlich ist das nicht gut für Mavericks, Mountain Lion usw., aber es ist großartig, dass dies endlich so funktioniert, wie es sollte, also markiere ich, dass dies die richtige Antwort ist, aber es könnte sich lohnen, es zu bearbeiten, da es nur unter 10.10 korrekt funktioniert .

Ihnen ist klar, dass Sie gerade ein Codefragment mit uns geteilt haben und es nicht klar ist, was Ihr Daemon eigentlich noch erreichen möchte, außer alle paar Sekunden eine Aktion auszuführen. Also werde ich einige Annahmen treffen, nur basierend auf dem, was Sie geschrieben haben.

  1. Es scheint, als würden Sie die Sperrdatei verwenden, um einen doppelten Start zu verhindern.
  2. Es scheint dann, dass Sie die Falle benötigen, um die Sperrdatei zu bereinigen, die zur Implementierung Ihres Tests verwendet wurde, um die Singularität sicherzustellen.
  3. Außerdem scheint es, dass Ihr Dämon eine Schlafschleife durchführt, um regelmäßig aufzuwachen und eine Aktion auszuführen. (Schlaf einfach mehr, in deinem Beispiel.)

Dies sind alles Probleme, die launchd unter Darwin (und damit OS X) besser lösen soll.

Was die Frage(n) mit dem Entladen und SIGTERM betrifft, speziell, wenn Sie unloadIhrem Launchdeamon ein SIGKILL anstelle eines SIGTERM senden. Wenn Sie den Job nur stoppen oder ihm ein SIGTERM senden möchten, verwenden Sie stopanstelle von unload.

Wenn Sie möchten, dass ein SIGTERM gesendet wird, unloadmüssen Sie möglicherweise EnableTransactions. Wenn Sie Bereinigungsaufgaben haben und möchten, dass Ihr Daemon Signale für die Bereinigung und SIGTERM empfängt, sollten Sie dies EnableTransactionsals Teil der launchd-Plist für Ihr Skript festlegen. <key>EnableTransactions</key><true/>. Dies wird in der Dokumentation unter https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html beschrieben

Aber die drei oben genannten Mechanismen sind unnötig, da launchd ...

Unter Darwin / OS X mit Launchdaemons ist die geeignete Methode zur Implementierung eines Schlafschleifen-Daemons die Verwendung, StartIntervalum in einem Intervall oder StartCalendarIntervalbasierend auf bestimmten Zeiten ausgeführt zu werden. Die zusätzliche Verwendung StartCalendarIntervalbietet den Vorteil, dass das System im Ruhezustand eine verpasste Intervallzeit ausführt, anstatt auf das nächste Intervall warten zu müssen, und ist in diesen Situationen im Allgemeinen das, was Sie wollen. Wenn Sie einen Job haben, den Sie nur aufrufen möchten, sollten Sie auch die Verwendung KeepAliveals Teil der Plist in Betracht ziehen.

Aus dem von Ihnen bereitgestellten Codebeispiel sieht es also so aus, als ob Sie nur alle 86400 Sekunden etwas ausführen möchten. Wenn dies der Fall ist, dann hat launchd einen Mechanismus dafür, den Sie stattdessen verwenden sollten und der Ihre Sperrdatei und Ihren Trap insgesamt überflüssig macht, da launchd so konzipiert ist, dass er all dies automatisch für Sie erledigt. Dieser Mechanismus ist StartIntervalund wenn er definiert ist, wird er Ihren Dämon alle N Sekunden starten. Launchd stellt auch sicher, dass nicht mehrere Kopien Ihres Daemons gestartet wurden.

Dieser Mechanismus wird in den Launchd-Dokumenten unter https://developer.apple.com/library/mac/documentation/Darwin/Reference/Manpages/man5/launchd.plist.5.html beschrieben , wo es heißt:

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.

Ihr darwinisiertes Skript ~/Downloads/Example.shwürde jetzt also ganz einfach so aussehen:

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

Und Ihre Plist würde in etwa so aussehen:

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

Beachten Sie, dass ich dies auch so angepasst habe, dass die Protokolldateien hier auf Darwin/launchd-ähnliche Weise und nicht im Skript selbst festgelegt werden. (Sie könnten sie natürlich entfernen und in Ihrem Skript behandeln, aber das ist angesichts von launchd nicht erforderlich.)

ProgramIch möchte anmerken, dass Sie dies auch folgendermaßen implementieren könnten :

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

Möglicherweise finden Sie auch http://launchd.info als nützliche Referenz zusammen mit den Apple-Dokumenten zur Funktionsweise von launchd unter https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ Einführung.html

Informationen zu regelmäßig ausgeführten Daemons finden Sie unter https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html#//apple_ref/doc/uid/10000172i-CH1-SW2

Entschuldigung, ich hätte sagen sollen, dass mein Skript eigentlich auf das Abmelden/Herunterfahren wartet, was dann an alle laufenden Agenten launchdgesendet wird (oder besser gesagt sollte). true wird nicht funktionieren, da Shell-Skripte die vproc-Befehle nicht aufrufen können, und die Einstellung auf false sollte die Standardeinstellung sein. Ich habe versucht, es als falsch einzufügen, um sicherzugehen, aber es scheint nicht zu helfen. SIGINTEnableTransactions
OK, das ist eine völlig andere Situation als das, was Ihr Skript anscheinend versucht hat, und eine, in der die von Ihnen erstellte Lösung eine komplexe Lösung für ein triviales Nichtproblem ist. Es gibt eine völlig bessere Möglichkeit, in OS X auf das Abmelden und Herunterfahren zu warten. EnableTransactionsFunktioniert auch für Signale entsprechend, unabhängig davon, ob der Prozess tatsächlich vproc verwendet oder nicht, dieser Dokumentationskommentar ist im Grunde für tatsächliche Anwendungen, aber er ist unabhängig davon funktional gleich.
Entschuldigung, das wurde gespeichert, bevor ich fertig war.
Sie können eine LaunchAgent-Erwartung für die Abmeldung erstellen oder einen Abmeldehaken ( sudo defaults write com.apple.loginwindow LogoutHook /Users/Shared/logoutHook.sh) verwenden.
Erfordert der Logout-Hook sudo oder kann er pro Benutzer durchgeführt werden? Richtige Art, auf Abmeldung zu achten oder nicht, ich bin immer noch ratlos, warum ich SIGINTmein Skript nicht so wie es ist eingibt; Konnten Sie das Problem reproduzieren, indem Sie den von mir bereitgestellten Beispielcode ausführen? Keine der Einstellungen für EnableTransactionsscheint für mich einen Unterschied zu machen.
Außerdem rät die Apple-Entwicklerdokumentation tatsächlich von der Verwendung von Login/Logout-Hooks ab. Soweit ich das beurteilen kann, ist die Verwendung eines schlafenden Skripts tatsächlich der "richtige" Weg, dies für einen Startagenten zu tun, außerdem gibt es andere Fälle, wie z. B. lang andauernde Jobs, bei denen die Erfassung immer noch wichtig ist SIGINT.

Sie sollten nicht festlegen, EnableTransactionses sei denn, Sie rufen aktiv vproc_transaction_beginbeim Start auf (was Sie nicht direkt von einem Shell-Skript aus tun können), gefolgt von vproc_transaction_endbeim Herunterfahren. Wenn Sie diese Einstellung aktivieren und dann nicht den ersten Aufruf tätigen, wird Ihr Skript als für eine plötzliche Beendigung geeignet markiert.

Sie möchten auch die Protokollierung implementieren, da Sie beim Herunterfahren ziemlich eingeschränkt sind launchdund sehen müssen, was fehlschlägt und warum!

Wenn Sie diese kombinieren, erhalten Sie Folgendes /Library/LaunchDaemons/org.example.shutdownhook.plist(an diesem Ort wird es automatisch beim Booten gestartet):

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

und das als /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

Das Obige schreibt alle 0-9 Sekunden zum Debuggen in das Protokoll. Verwenden Sie für die Verwendung ohne Debugging sleep $RANDOM & wait(0-32767 Sekunden Verzögerung) und bewegen Sie sich möglicherweise dolog "Running…"über die While-Schleife (abhängig davon, ob Sie regelmäßige Pings an das Protokoll senden möchten).

Sie können verwenden , um Nicht-Apple-sudo -Prozesse einschließlich dieses sudo launchctl list | grep -v com.applezu sehen , und Sie können die Protokolldatei sehen, um zu sehen, was vor sich geht (z. B. wenn Sie den Prozess beenden, können Sie sehen, dass er wegen neu gestartet wird ).launchdtail -fKeepAlive

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