launchd ist das Tool in MacOS X, wenn es darum geht, Prozesse im Hintergrund zu starten – sei es in gewissen Intervallen oder beim Systemstart.
Eigentlich war ich eher Fan von crontab, welches einfacher zu konfigurieren und verwalten war, allerdings ab MacOS X 10.5 durch das mächtigere launchd abgelöst wurde.

Immer wieder muss ich in ältere launchd-Konfigurationen hineinschauen, weil mir der eine oder andere Parameter entfallen ist, deshalb will ich den LaunchDaemons und LaunchAgents nun mal einen Artikel widmen.

Wo finde ich launchd?

launchd oder besser gesagt der launch daemon läuft bei MacOS X im Hintergrund und startet Prozesse, die in einem der folgenden Verzeichnisse konfiguriert wurden. Alle Konfigurationen für automatisch startende Prozesse sind .plist-Dateien im XML-Format.

Die Konfigurationen können sich sowohl in

/System/Library/LaunchAgents/
,
/System/Library/LaunchDaemons/
als auch in
/Library/LaunchAgents/
,
/Library/LaunchDaemons/
und
~/Library/LaunchAgents/
befinden.

Den Unterschied zwischen den einzelnen Library-Ordnern und von welchen Ordnern man lieber seine Finger lassen sollte, will ich hier nicht weiter ausführen.

Erstellt man einen neuen Job in

/Library/LaunchAgents/
respektive
/Library/LaunchDaemons/
sollte man darauf achten, dass man den Benutzer der .plist-Datei in
root:wheel
ändert.

Was ist der Unterschied zwischen einem LaunchDaemon und einem LaunchAgent?

LaunchDaemons können auch ausgeführt werden, wenn kein Benutzer am System angemeldet ist. Also, wenn der Computer eingeschaltet ist und nur das Login-Window zu sehen ist. Folglich können LaunchDaemons keine Informationen über das GUI anzeigen (Aus diesem Grund ist ein Verzeichnis
~/Library/LaunchDaemons/
nicht üblich).
LaunchDaemons werden (wenn in der .plist-Datei nicht anders angegeben) als root ausgeführt.

LaunchAgents laufen im Auftrag des angemeldeten Benutzers; somit muss ein Benutzer angemeldet sein, damit diese ausgeführt werden. Dafür können LaunchAgents Mitteilungen über den windowServer ausgeben.

Kleines launchctl 1x1

Starten und Stoppen

Nach einem Systemstart, respektive Anmelde-Vorgang, werden die .plist-Dateien in den LaunchAgents-/LaunchDaemons-Verzeichnissen geladen und die hinterlegten Prozesse gegebenenfalls gestartet.
Ich schreibe "gegebenenfalls", weil ein Starten des Prozess von verschiedenen Dingen wie Uhrzeit und Ordnerinhalten abhängig gemacht werden kann, auf die ich im Späteren noch eingehen werde.

Wenn man aber etwas austesten möchte oder einen neuen Job hinzufügt, muss man nicht gleich jedes Mal den Computer neu starten. Mit Hilfe von

launchctl
kann man die launchd-Jobs starten und beenden.

# Einen eben neu angelegten launchd-Job starten/aktivieren
# (wäre sonst erst nach dem Systemstart automatisch aktiv)
launchctl load ~/Library/LaunchAgents/net.apfelz.testjob.plist

# launchd-Job de-aktivieren (bis zum nächsten Systemstart)
launchctl unload ~/Library/LaunchAgents/net.apfelz.testjob.plist

Info: Bei LaunchDaemons/LaunchAgents im
/Library
-Verzeichnis sollte ein sudo vorangestellt werden, da diese zu Benutzer und Gruppe root:wheel gehören sollten!

Aktivieren und deaktivieren

launchd-Jobs können auch permanent de-aktiviert werden.
Statt die zugehörige .plist-Datei zu löschen, kann sie durch
launchctl unload -w ~/Library/LaunchAgents/net.apfelz.testjob.plist

bis auf weiteres deaktiviert werden.

Hierbei werden folgende beiden Zeilen in die .plist-Datei hineingeschrieben

<key>Disabled</key>
<true/>

Durch Eingriff in die .plist-Datei und entfernen der beiden Zeilen oder ändern des

<true/>
in ein
<false/>
oder durch den Befehl
launchctl load -w ~/Library/LaunchAgents/net.apfelz.testjob.plist

wird der Job wieder aktiviert/gestartet.

Jobs auflisten

Welche Jobs sind überhaupt aktiv? Dies sagt uns
launchctl list
(respektive
sudo launchctl list
, welches die Systemprozesse auflistet).

Ausgabe ist soetwas Ähnliches wie

PID     Status  Label
39842   -       0x10cde0.launchctl
39831   -       0x10ccf0.bash
39830   -       0x10cc00.sshd
39827   -       0x10cb10.sshd
165     -       com.apple.ScreenSharing.server
-       0       com.apple.ReportCrash.SafetyNet
-       0       com.apple.ReportCrash
-       0       com.apple.RemoteUI
-       0       com.apple.RemoteDesktop.agent
-       0       com.apple.quicklook
-       0       net.apfelz.nakamover
-       0       net.apfelz.lieferscheinablage
-       0       net.apfelz.ladelistenablage
-       0       net.apfelz.internetfreigabe
-       0       net.apfelz.farbanalyse
-       0       net.apfelz.delphin

Steht in der Spalte

PID
eine Nummer, so läuft dieser Prozess aktuell (und wäre über die angegebene Prozess-ID terminierbar, z.B.
kill 39842
).
Der Wert in der Spalte
Status
ist der exit Status des Jobs. Ein Status
0
bedeudet, dass der Prozess entweder bisher noch nicht aktiv war oder erfolgreich abgeschlossen wurde. Positive Werte werden für Fehler zurückgegeben und negative Werte bedeuten, dass der Prozess durch ein Signal beendet wurde (z.B.
-15
für ein SIGTERM).
Das
Label
ist nicht der Name der .plist-Datei, sondern der Wert, der innerhalb der Datei beim Tag Label hinterlegt ist. Es ist natürlich empfohlen, das Label analog zum Dateinamen zu halten (also RemoteUI als Wert für Label, wenn die Datei com.apple.RemoteUI.plist heißt).

Da die Liste der Jobs immer sehr umfangreich ist, benenne ich meine launchd-plist-Dateien immer nach dem Schema net.apfelz.xxxxx.plist.
Wenn ich nur einen Überblick über die eigenen Jobs haben will, kann ich die Ausgabe dank

grep
einschränken:
launchctl list | grep apfelz

Über die ersten vier Einträge meiner oberen Liste mit den kryptischen Bezeichnungen sagt das launchd-Manual übrigens, dass es sich um Jobs handelt, die nicht von launchd gemanaged werden, aber an irgendeinem Punkt eine Anfrage an launchd gestellt haben. Dies sind eigentlich immer Systemprozesse.

Mit dem List-Befehl kann man sich auch eine detailliertere Info über einen launchd-Job einholen:

launchctl list net.apfelz.nakamover

Gibt Folgendes aus:

{
        "Label" = "net.apfelz.nakamover";
        "LimitLoadToSessionType" = "Aqua";
        "OnDemand" = true;
        "LastExitStatus" = 0;
        "TimeOut" = 30;
        "ProgramArguments" = (
                "/Library/apfelzscripts/naka-mover.pl";
        );
};

Genereller Aufbau einer launchd-plist-Datei

Ausführung einmal pro Stunde

<?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>net.apfelz.meinLaunchdJob</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Library/apfelzScripts/launchd-testscript.sh</string>
        </array>
        <key>StandardOutPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StandardErrorPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StartInterval</key>
        <integer>3600</integer>
</dict>
</plist>

(Während in den nächsten Beispielen auf eine genaue Uhrzeit eingegangen wird, ist in obigem Beispiel immer von einem Intervall ab dem ersten Starten des Jobs auszugehen)

Das Label sollte wie zuvor schon erwähnt dem Dateinamen der .plist-Datei entsprechen, um Verwirrungen zu vermeiden.

StandardOutPath und StandardErrorPath sind optional, gebe ich allerdings immer an, um zu Debug-Zwecken die Skript-Ausgabe eine Datei umzuleiten.

Statt ProgramArguments kann in diesem Fall auch Program verwendet werden. Das Skript sollte dann ohne umschließendes

<array>
folgen. Darauf gehe ich im Späteren noch ein.

Ausführung zur vollen und halben Stunde

<?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>net.apfelz.meinLaunchdJob</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Library/apfelzScripts/launchd-testscript.sh</string>
        </array>
        <key>StandardOutPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StandardErrorPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StartCalendarInterval</key>
        <array>
                <dict>
                <key>Minute</key>
                <integer>0</integer>
                </dict>
                <dict>
                <key>Minute</key>
                <integer>30</integer>
                </dict>
        </array>
</dict>
</plist>

Folgende Schlüsselwerte können bei StartCalendarInterval verwendet werden (siehe auch folgende Beispiele)

  • Minute
  • Hour
  • Day
  • Weekday (Sonntag: 0 und 7)
  • Month

Wie im oberen Beispiel zu sehen, können die Schlüsselfelder auch mehrfach verwendet werden, um eine mehrfache Ausführung auszulösen (also Minute 0 und Minute 30).

Befindet sich der Computer zu dieser Zeit gerade im Ruhezustand, wird die Ausführung ausgelöst, sobald er aus dem Ruhezustand erwacht. Sollten der Job bis dahin eigentlich mehrmals ausgeführt worden sein, wird er jedoch nur einmal ausgeführt.

Ausführung am Ersten des Monats um Zwölf Uhr mittags

<?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>net.apfelz.meinLaunchdJob</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Library/apfelzScripts/launchd-testscript.sh</string>
        </array>
        <key>StandardOutPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StandardErrorPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StartCalendarInterval</key>
        <dict>
                <key>Hour</key>
                <integer>12</integer>
                <key>Minute</key>
                <integer>00</integer>
                <key>Day</key>
                <integer>1</integer>
        </dict>
</dict>
</plist>

Das umschließende

<array>
aus dem vorhergehenden Beispiel wurde hier weggelassen, da nur ein Zeitpunkt für den Start mitgegeben wurde.

Ausführung sonntags um Acht Uhr morgens

<?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>net.apfelz.meinLaunchdJob</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Library/apfelzScripts/launchd-testscript.sh</string>
        </array>
        <key>StandardOutPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StandardErrorPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StartCalendarInterval</key>
        <dict>
                <key>Hour</key>
                <integer>8</integer>
                <key>Minute</key>
                <integer>00</integer>
                <key>Weekday</key>
                <integer>7</integer>
        </dict>
</dict>
</plist>

(launchd akzeptiert bei Weekday für den Sonntag sowohl eine 0 wie auch eine 7)

Bei einer neuen/geänderten Datei in einem bestimmten Verzeichnis (=> Hotfolder)

<?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>net.apfelz.meinLaunchdJob</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Library/apfelzScripts/launchd-testscript.sh</string>
        </array>
        <key>StandardOutPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StandardErrorPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>WatchPaths</key>
        <array>
                <string>/Users/apfelz/Desktop/meinHotfolder</string>
        </array>
</dict>
</plist>

Neben WatchPaths gibt es auch noch QueueDirectories.
Unterschied:

  • Bei WatchPaths wird das Skript ausgeführt, wenn sich am Inhalt des Ordners etwas ändert
  • Bei QueueDirectories wird das Skript ausgeführt, sofern Dateien im Verzeichnis vorhanden sind. Das Verzeichnis sollte also optimalerweise nach der Ausführung geleert werden, um keine Endlosschleife zu produzieren (außer die abgelegten Dateien sollen so lange bearbeitet werden, so lange sie sich im Verzeichnis befinden).

Beim Booten/Login starten

<?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>KeepAlive</key>
        <true/>
        <key>Label</key>
        <string>net.apfelz.meinLaunchdJob</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Library/apfelzScripts/launchd-testscript.sh</string>
        </array>
        <key>StandardOutPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
        <key>StandardErrorPath</key>
        <string>/Library/Logs/apfelz/launchd-testscript.txt</string>
</dict>
</plist>

Eigentlich analog zu den vorherigen .plist-Dateien, nur ohne StartCalendarInterval, StartInterval oder WatchPaths.

Durch die Zugabe von KeepAlive versucht launchd, das angegebene Skript am Laufen zu halten; startet es also im Fehlerfall erneut.

Statt einem

<true/>
 kann auch ein Dictionary von Elementen angegeben werden. Mehr dazu auf im launchd-Manual von Apple.

Weitere interessante Optionen

<key>GroupName</key>
<string>staff</string>
<key>UserName</key>
<string>_www</string>

Sofern die .plist-Datei im Verzeichnis

/Library/LaunchDaemons
liegt (und somit mit root-Rechten und nicht wie ein LaunchAgent benutzerabhängig ausgeführt wird), kann so der Gruppen- und Benutzername mitgegeben werden, unter denen das aufgeführte Skript laufen soll.

Achtung: Der Benutzer muss Schreibberechtigung für die log-Dateien haben, welcher unter StandardOutPath und StandardErrorPath angegeben sind, sonst wird der Task mit ominösen Fehlermeldungen im system.log nicht ausgeführt!
Siehe dieser Artikel.


<key>WorkingDirectory</key>
<string>/Library/apfelzscripts/reader/</string>

Pfad, zu welchem ge
ch
dir't werden soll, bevor das aufgeführte Skript ausgeführt wird (erspart einem, im Skript immer in absoluten Pfaden zu schreiben).


<key>EnvironmentVariables</key>
<dict>
        <key>ENVIRONMENT_VARIABLE_NAME</key>
        <string>wert</string>
</dict>

Variablen wie etwa $home können an dieser Stelle allerdings nicht verwendet werden.


Wichtiges zu den ProgramArguments

Möchte ich via launchd ein Skript ausführen, so sollte der Aufruf des Skriptes in der .plist-XML-Datei eigentlich so aussehen:

<key>Program</key>
<string>/Library/apfelzScripts/launchd-testscript.sh</string>

Argumente, die dem Skript übergeben werden, werden nicht direkt in diesen einen

<string>
-Tag geschrieben, sondern als einzelne separate Argumente aufgeführt.

Also anstatt wie auf der Kommandozeile

/Library/apfelzScripts/launchd-testscript.sh -a-o /my/outpath/

ist in der .plist-XML-Datei zu schreiben:

<key>Program</key>
<string>/Library/apfelzScripts/launchd-testscript.sh</string>
<key>ProgramArguments</key>
<array>
        <string>-a</string>
        <string>-o</string>
        <string>/my/outpath/</string>
</array>

Der extra Schlüssel Program kann auch weggelassen werden, somit wird angenommen, dass der erste string unter ProgramArguments das eigentliche Programm ist:

<key>ProgramArguments</key>
<array>
        <string>/Library/apfelzScripts/launchd-testscript.sh</string>
        <string>-a</string>
        <string>-o</string>
        <string>/my/outpath/</string>
</array>

(Diese Umstände machen launchd für mich im Gegensatz zu crontab nicht wirklich attraktiver)

Kleine Helferlein

Mit Lingon und LaunchControl lassen sich die .plist-Dateien mit einem GUI verwalten.

Diese Tools sind sicherlich einen Blick wert.