Mit Launchd Skripte auf dem Mac automatisieren
Mit dem LaunchDaemon-Dienst unter macOS haben Sie auf dem Mac die Möglichkeit, Skripte und Programme automatisch zu starten. Der Start eines Skripts kann sowohl nach zeitlichen Kriterien als auch auf bestimmte Ereignisse hin erfolgen.
Im ersten Abschnitt dieses Artikels wird erklärt, wie man Skripte für die automatische Ausführung vorbereitet und mit den nötigen Tools im Terminal umgeht. Im zweiten Teil finden Sie einige plist-Dateien als Praxis-Beispiele, die Sie für Ihren Mac anpassen können, um so Ihre eigenen Skripte zeit- oder ereignisgesteuert zu starten.
Hier finden Sie ohne Umwege die plist-Beispiele, die in
/Library/LaunchDaemons
eingebunden werden können.
Dieser Artikel geht davon aus, dass Sie mit dem Terminal grundsätzlich vertraut sind. Alle Befehle, die hier Sie hier finden, werden im Terminal eingegeben.
Vorbereitungen und Umgang mit Launchd
Zunächst müssen wir ein Skript erstellen, das später mit LaunchDaemon ausgeführt wird. Es spielt dabei keine Rolle, ob es sich um ein Bash-, Perl- oder Python-Skript handelt – in diesem Beispiel erstellen wir zur Demonstration ein einfaches Bash-Skript, das auch gleich einen einfachen Logging-Mechanismus eingebaut hat, um die Skript-Ausführung mit launchd prüfen zu können.
Das Verzeichnis /usr/local/bin anlegen
Das Verzeichnis /usr/local/bin
ist traditionell der bevorzugte Ort, um Skripte in macOS
abzulegen. Viele Tools und Programme nutzen dieses Verzeichnis, allerdings erstellt Apple es nicht bei
der System-Installation von macOS.
Wenn Sie sich nicht sicher sind, ob das Verzeichnis bereits existiert, geben Sie einfach ein:
cd /usr/local/bin
Falls Sie bie der Eingabe des cd-Befehls oben die Fehlermeldung cd: no such file or directory:
/usr/local/bin
erhalten, existiert das Verzeichnis /usr/local/bin
noch nicht. In
diesem Fall erstellen Sie zunächst das Verzeichnis und setzen anschließend die passenden Userrechte
(geben Sie Ihr Benutzer-Passwort bei der Passwort-Abfrage ein):
mkdir -p /usr/local/bin
sudo chown $(whoami) /usr/local/bin
Password:
Ein Shell-Skript erstellen, das ausgeführt werden soll
Erstellen Sie nun ein Skript, dass später von LaunchDaemon ausgeführt wird.
In diesem Beispiel
verwenden wir den Editor nano, der bei macOS mitgeliefert wird und auch für Einsteiger
einfach zu bedienen ist. Wenn Sie erfahren im Umgang mit der Shell sind, werden Sie sicher einen
anderen Editoren wie etwa vi bevorzugen.
nano /usr/local/bin/script_to_start.sh
Fügen Sie nun im Editor folgenden Inhalt in das Skript ein:
#!/bin/bash logfile="/var/log/script_to_start.log" datestr=$(date +%d.%m.%Y-%H:%M:%S) echo $datestr": Testskript wird ausgeführt. Parameter #1: "$1", Parameter #2: "$2", parameter3: "$3". "Username: $(whoami) >> $logfile(Falls Sie nano verwenden, beenden Sie den Editor mit der Tastenkombination
CTRL-X
,
drücken dann y
für Yes und quittieren anschließend mit Enter
den Dateinamen,
um nano zu beenden.)
Machen Sie nun das gerade erstellte Skript ausführbar:
chmod +x /usr/local/bin/script_to_start.sh
Ein Logfile für das Skript erstellen
Damit die Ausführung des Skriptes kein Blindflug wird, sollte in einem Logfile jeder Skriptstart und ggf. weitere Informationen protokolliert werden.
Legen Sie dazu in /var/log
zunächst eine leere Datei an und stellen deren Rechte so
ein, dass das Skript des eingeloggten Users in die Logdatei schreiben darf:
sudo touch /var/log/script_to_start.log
sudo chown $(whoami) /var/log/script_to_start.log
Selbstverständlich muss die Log-Datei nicht script_to_start.log heißen – falls Sie einen anderen Namen bevorzugen, müssen Sie diesen aber auch im Skript oben anpassen.
Das Shell-Skript testen
Starten Sie nun das Skript manuell im Terminal. Wir geben zusätzlich beim Skriptaufruf noch zwei Parameter mit, um später zu zeigen, wie die Parameterübergabe in den plist-Dateien für Launchd funktioniert.
/usr/local/bin/script_to_start.sh Parameter1 Parameter2
Es sollte keine Fehlermeldung erscheinen. Im Logfile sollte ein passender Eintrag vorhanden sein:
tail /var/log/script_to_start.log
07.06-2017-14:45:46: Testskript wird ausgeführt. Parameter #1: Parameter1, Parameter #2: Parameter2. myusername
plist-Dateien mit launchctl starten, stoppen und auflisten
Jetzt, wo alle Vorbereitungen erledigt sind, kann es losgehen mit dem Einbinden des Skriptes
mit LaunchDaemon.
Hierzu wird eine plist-Datei im Verzeichnis
/Library/LaunchDaemons
erstellt, in der festgelegt wird, wann das Skript in
/usr/local/bin gestartet werden soll.
Im Verzeichnis /Library/Launchdaemons
werden mit dem Tool launchctl
die
Launchd-Jobs verwaltet.
Wechseln Sie also zunächst ins Verzeichnis /Library/LaunchDaemons
:
cd /Library/LaunchDaemons
Die Dateien in diesem Verzeichnis gehören dem root-User. Erstellen Sie daher mittels sudo das erste Startskript:
sudo nano de.ulrich-zentis.5_min_interval.plist
Fügen Sie folgenden Inhalt ein und speichern Sie die Datei (CTRL-X
, y
,
Enter
). Diese Demo-plist ist das erste einfache Beispiel aus der Sammlung
unten.
<?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>de.ulrich-zentis.5_min_interval</string> <key>Program</key> <string>/usr/local/bin/script_to_start.sh</string> <key>StartInterval</key> <integer>300</integer> <key>UserName</key> <string>myusername</string> </dict> </plist>
Dieser Befehl lädt eine plist-Datei in LaunchDaemon:
sudo launchctl load -w de.ulrich-zentis.5_min_interval.plist
-w
sagt LaunchDaemon, dass das Skript persistent behandelt wird, also unter anderem auch
nach einem Neustart des Mac geladen werden soll. Sollten Sie ein Fehlermeldung erhalten, können Sie
mit tail /var/log/system.log
in den letzten Zeilen des Systemlog nachschauen, ob
launchd etwas Konkretes bemängelt.
Mit unload
stoppen Sie die automatische Ausführung eines Skriptes wieder:
sudo launchctl unload de.ulrich-zentis.5_min_interval.plist
Eine Liste der gerade aktiven Launchd-Jobs erhalten Sie mit:
sudo launchctl list
Wobei die Liste lang ist und schnell die Übersicht verloren geht. Um die Liste zu kürzen und nur die
Jobs anzuzeigen, die etwa mit de.
beginnen, hilft grep
:
sudo launchctl list | grep de.
Beispiel-Skripte für LaunchDaemon
In diesem Abschnitt finden Sie einige plist-Dateien für Launchd, mit denen Sie Skripte auf Ihrem Mac automatisch starten können.
Der vordere Teil des Namens der plist-Datei kann natürlich frei
gewählt werden, allerdings sollte die Referenz unter Label
(in diesem Beispiel
de.ulrich-zentis.5_min_interval
) innerhalb der plist-Datei auch entsprechend
angepasst werden.
Fügen Sie nun also den Inhalt aus dem folgenden Fenster in die in nano geöffnete plist-Datei ein und
speichern Sie sie anschließend ab
(in nano: CTRL-X
, y
, Enter
):
Skripte in einem Zeitintervall starten
Hier die erste plist-Datei de.ulrich-zentis.5_min_interval.plist
:
<?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>de.ulrich-zentis.5_min_interval</string> <key>Program</key> <string>/usr/local/bin/script_to_start.sh</string> <key>StartInterval</key> <integer>300</integer> <key>UserName</key> <string>myusername</string> </dict> </plist>
Der richtige Username
Der Username in jedem Beispiel dieser Seite muss unter dem Key UserName
(hier:
myusername
) durch den korrekten Usernamen ersetzt werden, der das Skript auf Ihrem Mac
ausführen soll:
[...] <key>UserName</key> <string>IhrUserKurzname</string> [...]
Wenn Sie den Kurznamen des Users nicht wissen, liefert Ihnen die Eingabe von
echo $(whoami)
im Terminal den Usernamen des gerade eingeloggten Users.
Ein Skript ohne Parameter ausführen
Diese plist-Datei de.ulrich-zentis.5_min_interval.plist
führt ein Skript ohne weitere
Parameter alle 300 Sekunden aus:
<?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>de.ulrich-zentis.5_min_interval</string> <key>Program</key> <string>/usr/local/bin/script_to_start.sh</string> <key>StartInterval</key> <integer>300</integer> <key>UserName</key> <string>myusername</string> </dict> </plist>
Der Key Program
gibt den vollständigen Pfad des Skriptes an, das ausgeführt werden soll.
Parameter für das Skript werden in diesem Fall nicht mitgegeben.
Die Angabe unter Startinterval
gibt das Startintervall in Sekunden an, siehe
hier.
Ein Skript mit Parametern ausführen
Diese plist-Datei de.ulrich-zentis.5_min_interval_w_params.plist
führt ein Skript mit
zwei Parametern aus. Die Ausführung erfolgt in diesem Beispiel alle 300 Sekunden.
<?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>de.ulrich-zentis.5_min_interval_w_params</string> <key>ProgramArguments</key> <array> <string>/usr/local/bin/script_to_start.sh</string> <string>Param1</string> <string>Param2</string> </array> <key>StartInterval</key> <integer>300</integer> <key>UserName</key> <string>myusername</string> </dict> </plist>
Der Username unter dem Key UserName
(hier: myusername
) muss durch den
entsprechenden Usernamen ersetzt werden.
Die Angabe unter Startinterval
gibt das Startintervall in Sekunden an.
Das Label ProgramArguments
wird anstelle von Program
im Beispiel oben
verwendet, wenn Parameter für das Skript mitgegeben werden sollen:
Ohne Parameter: [...] <key>Program</key> <string>/usr/local/bin/script_to_start.sh</string> [...] Mit Parametern: [...] <key>ProgramArguments</key> <array> <string>/usr/local/bin/script_to_start.sh</string> <string>Param1</string> <string>Param2</string> </array> [...]
In diesem Beispiel wird das Skript mit zwei Paramtern gestartet, wie das etwa beim Befehl cp
sourcefile.txt targetfile.txt
in der Shell der Fall wäre. Weitere Parameter werden dem Array
einfach durch eine weitere Zeile mit <string></string>
-Tags hinzugefügt.
Ausführung in einem Zeitintervall
Im ersten Beispiel oben werden die Skripte mit dem Label Startinterval
gestartet. Dies
ist die wohl einfachste Art, wie ein Skript mit Lauchd ausgeführt werden kann – für die meisten
Fälle sollte dies völlig ausreichen.
[...] <key>StartInterval</key> <integer>300</integer> [...]
Gezählt wird in Sekunden. In unserem Beispiel wird das Skript alle 5 Minuten (also alle 300 Sekunden, Sherlock) ausgeführt.
Ein Skript zu einer bestimmten Uhrzeit / einem bestimmten Datum ausführen
Diese plist-Datei de.ulrich-zentis.specific_datetime.plist
führt ein Skript mit zwei
Parametern am 15. jeden Monats um 22:00 Uhr aus.
<?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>de.ulrich-zentis.specific_datetime</string> <key>Program</key> <string>/usr/local/bin/script_to_start.sh</string> <key>StartCalendarInterval</key> <dict> <key>Minute</key> <integer>00</integer> <key>Hour</key> <integer>22</integer> <key>Day</key> <integer>15</integer> </dict> <key>UserName</key> <string>myusername</string> </dict> </plist>
Der Username unter dem Key UserName
(hier: myusername
) muss durch den
entsprechenden Usernamen ersetzt werden.
Soll das Skript zu jeder vollen Stunde ausgeführt werden, wird lediglich der Parameter
Minute
angegeben:
[...] <key>StartCalendarInterval</key> <dict> <key>Minute</key> <integer>00</integer> </dict> [...]
Analog wird das Skript nur einmal am Tag ausgeführt, wenn nur Minute
und Hour
angegeben werden:
[...] <key>StartCalendarInterval</key> <dict> <key>Minute</key> <integer>00</integer> <key>Hour</key> <integer>22</integer> </dict> [...]
Es gibt noch weitere Zeitangaben, die für StartCalendarInterval
verwendet werden können:
Month
und CalendarDay
. Month
ist natürlich für die Ausführung
in einem bestimmten Monat zuständig, während CalendarDay
einen bestimmten Wochentag
angibt. Die Zählung startet Sonntags mit 0
, folgende Zeitangabe startet das Skript
beispielsweise jeden Donnerstag zu jeder vollen Stunde:
[...] <key>StartCalendarInterval</key> <dict> <key>Minute</key> <integer>00</integer> <key>CalendarDay</key> <integer>4</integer> </dict> [...]
Der Vollständigkeit halber sei erwähnt, dass mehrere Zeitangaben einer Art in einem
Array of Dicts kombinierbar sind – theoretisch. Mit einem Array können Skripte zu mehreren bestimmten
Minuten oder Stunden gestartet werden. Leider ist aufgrund der miesen Implementierung durch Apple
eine Kombination mit anderen Keys nicht möglich bzw. führt zu unvorhersehbaren Ergebnissen, wie
etwa einer unbeabsichtigten minütlichen Ausführung. Dieser Abschnitt startet beipielsweise ein
Skript zur Minute 30 und 33, eine Kombination mit anderen Keys innerhalb von
CalendarInterval
sollte aber wegen der erwähnten Probleme tunlichst vermieden bzw.
ausgiebig getestet werden, sofern launchctl
das Skript überhaupt akzeptiert.
[...] <key>StartCalendarInterval</key> <array> <dict> <key>Minute</key> <integer>30</integer> </dict> <dict> <key>Minute</key> <integer>33</integer> </dict> </array> [...]
Dieses Beispiel der Nicht-Kombinierbarkeit zeigt, dass das gut abgehangene cron durchaus seine Vorzüge hat und XML nicht notwendigerweise Vorteile bringt.
RunAtLoad - Ausführung beim Laden und Booten
Der Key RunAtLoad
wird in Beispielen sehr häufig verwendet, weil er zur Fehlerdignose
sehr praktisch ist – das Skript wird damit bereits beim Laden mit
launchctl load skriptname.plist
zum ersten Mal ausgeführt, was eine sofortige
Reaktion im Logfile zur Folge hat. Auf diese Weise kann sofort geprüft werden, ob sich das Skript so
benimmt wie erwartet.
[...] <key>RunAtLoad</key> <true/> [...]
Allerdings wird dann auch beim Booten des Macs das Skript ausgeführt, was zu unerwarteten Effekten führen kann – insbesondere wenn das Skript nur laufen soll, wenn Veränderungen in einem Verzeichnis registriert werden, kann dies zu unerwünschten Nebeneffekten führen.
Ein Skript bei Veränderungen in einem Ordner ausführen
Eine sehr praktische Startmethode bietet WatchPaths
. Damit wird ein Skript durch
Auswertung sogenannter Filesystem Events gestartet, sobald in einem bestimmten Ordner
Veränderungen registriert werden. Werden beispielsweise Dateien oder weitere Ordner hinzugefügt,
gelöscht oder verändert, startet das eingetragene Skript.
Diese Startmethode ist extrem praktisch für Backupjobs, Datentranfers oder interaktive Workflows, die indirekt durch den Anwender gestartet werden sollen. So kann durch Abspeichern einer Medien- oder Projektdatei in einem Grafik- oder Audioprogramm eine Konvertierung oder ein Datentransfer angestossen werden, ohne dass auf einen bestimmten Zeitpunkt gewartet werden muss.
Hier die de.ulrich-zentis.watchfolder.plist
, die den Ordner WatchFolder
auf
dem Schreibtisch des Benutzers auf Veränderungen überwacht:
<?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>de.ulrich-zentis.watchfolder</string> <key>Program</key> <string>/usr/local/bin/script_to_start.sh</string> <key>WatchPaths</key> <array> <string>/Users/myusername/Desktop/WatchFolder</string> </array> <key>UserName</key> <string>myusername</string> </dict> </plist>
Natürlich muss neben der gewohnten Anpassung des Usernamens unter dem Key UserName
hier
zusätzlich noch der korrekte Username (hier: myusername) im Pfad des WatchFolder
<string>/Users/myusername/Desktop/WatchFolder</string>
angepasst werden.