Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cronjobs & launchd

Automatisierte Aufgaben sind essentiell für Systemadministration, Backups, Wartung und wiederkehrende Prozesse. Unter macOS gibt es zwei Systeme: das klassische Unix-Tool cron und Apples eigenes launchd.

1 Cron-Grundlagen

cron ist der klassische Unix-Dienst für zeitgesteuerte Aufgaben. Er stammt aus den 1970er Jahren und ist auf praktisch jedem Unix/Linux-System verfügbar.

1.1 Wie cron funktioniert

  • Der cron-Daemon läuft im Hintergrund
  • Er prüft jede Minute, ob geplante Aufgaben anstehen
  • Aufgaben werden in Crontabs (cron tables) definiert
  • Jeder Benutzer kann eine eigene Crontab haben

1.2 Crontab verwalten

BefehlBeschreibung
crontab -eCrontab bearbeiten
crontab -lCrontab anzeigen
crontab -rCrontab löschen
crontab -u user -eCrontab eines anderen Benutzers bearbeiten (root)

Erste Verwendung:

crontab -e

Dies öffnet den Standard-Editor (meist vim oder nano). Um den Editor zu ändern:

export EDITOR=nano
crontab -e

1.3 Speicherorte

PfadBeschreibung
/var/at/tabs/Benutzer-Crontabs (macOS)
/etc/crontabSystem-Crontab
/etc/cron.d/Zusätzliche System-Crontabs
/etc/cron.daily/Täglich ausgeführte Skripte
/etc/cron.weekly/Wöchentlich ausgeführte Skripte
/etc/cron.monthly/Monatlich ausgeführte Skripte

warning

Unter macOS existieren die /etc/cron.*-Verzeichnisse standardmäßig nicht, da Apple launchd bevorzugt.

2 Cron-Syntax & Beispiele

2.1 Grundsyntax

Eine Crontab-Zeile hat folgendes Format:

┌───────────── Minute (0-59)
│ ┌───────────── Stunde (0-23)
│ │ ┌───────────── Tag des Monats (1-31)
│ │ │ ┌───────────── Monat (1-12)
│ │ │ │ ┌───────────── Wochentag (0-7, 0 und 7 = Sonntag)
│ │ │ │ │
* * * * * Befehl

2.2 Sonderzeichen

ZeichenBedeutungBeispiel
*Jeder Wert* * * * * = jede Minute
,Liste von Werten1,15,30 = Minute 1, 15 und 30
-Bereich1-5 = Montag bis Freitag
/Schrittweite*/15 = alle 15 Einheiten

2.3 Beispiele

Zeitangaben:

AusdruckBedeutung
* * * * *Jede Minute
0 * * * *Jede Stunde (zur vollen Stunde)
0 0 * * *Täglich um Mitternacht
0 6 * * *Täglich um 6:00 Uhr
30 8 * * 1-5Mo–Fr um 8:30 Uhr
0 0 * * 0Jeden Sonntag um Mitternacht
0 0 1 * *Am 1. jeden Monats um Mitternacht
0 0 1 1 *Am 1. Januar um Mitternacht
*/15 * * * *Alle 15 Minuten
0 */2 * * *Alle 2 Stunden
0 9-17 * * 1-5Mo–Fr, stündlich von 9–17 Uhr

Vollständige Crontab-Beispiele:

# Backup jeden Tag um 2:00 Uhr
0 2 * * * /Users/max/scripts/backup.sh

# Log-Rotation jeden Sonntag um 3:00 Uhr
0 3 * * 0 /Users/max/scripts/rotate-logs.sh

# Alle 5 Minuten: System-Check
*/5 * * * * /Users/max/scripts/health-check.sh

# Werktags um 9:00: Bericht generieren
0 9 * * 1-5 /Users/max/scripts/daily-report.sh

# Am 1. und 15. jeden Monats
0 0 1,15 * * /Users/max/scripts/bi-monthly.sh

# Nur im Januar, täglich um 6:00
0 6 * 1 * /Users/max/scripts/january-task.sh

2.4 Spezielle Strings

Einige cron-Implementierungen unterstützen lesbare Kürzel:

StringÄquivalentBedeutung
@rebootBei Systemstart
@yearly0 0 1 1 *Jährlich
@monthly0 0 1 * *Monatlich
@weekly0 0 * * 0Wöchentlich
@daily0 0 * * *Täglich
@hourly0 * * * *Stündlich
@daily /Users/max/scripts/backup.sh
@reboot /Users/max/scripts/startup.sh

macOS-Kompatibilität:

macOS verwendet Vixie cron (BSD-basiert). Die Unterstützung der speziellen Strings:

StringmacOSAnmerkung
@yearlyFunktioniert
@monthlyFunktioniert
@weeklyFunktioniert
@dailyFunktioniert
@hourlyFunktioniert
@rebootNicht zuverlässig

Problem mit @reboot: Unter macOS startet der cron-Daemon erst nach dem Benutzer-Login, nicht beim Systemstart. Jobs mit @reboot werden daher nicht ausgeführt oder nur beim ersten Login nach einem Neustart.

Lösung für Startup-Tasks: Verwende stattdessen launchd mit RunAtLoad:

<key>RunAtLoad</key>
<true/>

Siehe [[#4 Erstellung von .plist-Jobs|Erstellung von .plist-Jobs]] für Details.

2.5 Umgebungsvariablen

Cron führt Befehle mit einer minimalen Umgebung aus. Wichtige Variablen sollten definiert werden:

# Umgebungsvariablen setzen
SHELL=/bin/zsh
PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin
HOME=/Users/max
MAILTO=max@example.com

# Jobs
0 * * * * /Users/max/scripts/task.sh

Wichtig: Der PATH in cron ist sehr eingeschränkt. Entweder:

  1. Vollständige Pfade zu Programmen verwenden
  2. PATH in der Crontab setzen
  3. PATH im Skript selbst setzen
#!/bin/zsh
# Am Anfang des Skripts:
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

2.6 Ausgabe und Logging

Standardmäßig wird die Ausgabe per E-Mail gesendet (falls konfiguriert). Alternativen:

# Ausgabe in Datei
0 * * * * /script.sh >> /var/log/script.log 2>&1

# Ausgabe verwerfen
0 * * * * /script.sh > /dev/null 2>&1

# Nur Fehler loggen
0 * * * * /script.sh >> /var/log/script.log 2>&1

# Mit Zeitstempel
0 * * * * /script.sh 2>&1 | while read line; do echo "$(date): $line"; done >> /var/log/script.log

Logging-Wrapper-Skript:

#!/bin/zsh
# /Users/max/scripts/run-with-log.sh

LOGFILE="/var/log/cron-jobs.log"
SCRIPT="$1"
shift

echo "=== $(date '+%Y-%m-%d %H:%M:%S') - Start: $SCRIPT ===" >> "$LOGFILE"
"$SCRIPT" "$@" >> "$LOGFILE" 2>&1
EXIT_CODE=$?
echo "=== $(date '+%Y-%m-%d %H:%M:%S') - Ende: $SCRIPT (Exit: $EXIT_CODE) ===" >> "$LOGFILE"
echo "" >> "$LOGFILE"

Verwendung in Crontab:

0 * * * * /Users/max/scripts/run-with-log.sh /Users/max/scripts/task.sh

2.7 Häufige Probleme

1. Skript läuft nicht:

# Skript ausführbar machen
chmod +x /Users/max/scripts/script.sh

# Shebang prüfen
head -1 /Users/max/scripts/script.sh
# Sollte sein: #!/bin/zsh oder #!/bin/bash

2. Befehl nicht gefunden:

# FALSCH: brew ist nicht im PATH
0 * * * * brew update

# RICHTIG: Vollständiger Pfad
0 * * * * /opt/homebrew/bin/brew update

3. Berechtigungsprobleme:

# cron-Logs prüfen (macOS)
log show --predicate 'subsystem == "com.apple.cron"' --last 1h

3 launchd unter macOS

launchd ist Apples Init-System und Dienst-Manager. Es ist leistungsfähiger als cron und der empfohlene Weg für geplante Aufgaben unter macOS.

3.1 Konzepte

Agents vs. Daemons:

TypLäuft alsSpeicherortBeschreibung
User AgentAktueller Benutzer~/Library/LaunchAgents/Benutzer-spezifische Aufgaben
Global AgentAktueller Benutzer/Library/LaunchAgents/Für alle Benutzer, aber im Benutzerkontext
Global Daemonroot/Library/LaunchDaemons/Systemweite Dienste
System Daemonroot/System/Library/LaunchDaemons/macOS-Systemdienste (nicht bearbeiten!)

Für die meisten Benutzeraufgaben: ~/Library/LaunchAgents/

3.2 launchctl – Das Verwaltungstool

BefehlBeschreibung
launchctl listAlle geladenen Jobs anzeigen
`launchctl listgrep LABEL`
launchctl load FILE.plistJob laden und aktivieren
launchctl unload FILE.plistJob deaktivieren und entladen
launchctl start LABELJob sofort ausführen
launchctl stop LABELJob stoppen
launchctl kickstart gui/$(id -u)/LABELJob neu starten (moderne Syntax)

Moderne Syntax (ab macOS 10.10):

# Benutzer-Agent laden
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.task.plist

# Benutzer-Agent entladen
launchctl bootout gui/$(id -u)/com.user.task

# System-Daemon laden (als root)
sudo launchctl bootstrap system /Library/LaunchDaemons/com.company.daemon.plist

3.3 Job-Status prüfen

# Alle Jobs auflisten
launchctl list

# Bestimmten Job finden
launchctl list | grep -i backup

# Job-Details anzeigen (moderne Syntax)
launchctl print gui/$(id -u)/com.user.backup

# Fehler prüfen
launchctl error <error_code>

4 Erstellung von .plist-Jobs

Property List (.plist) Dateien definieren launchd-Jobs im XML-Format.

4.1 Grundstruktur

<?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>com.benutzername.jobname</string>

    <key>ProgramArguments</key>
    <array>
        <string>/pfad/zum/skript.sh</string>
    </array>

    <!-- Zeitplan oder Auslöser hier -->

</dict>
</plist>

4.2 Wichtige Schlüssel

Identifikation:

SchlüsselTypBeschreibung
LabelStringEindeutiger Bezeichner (erforderlich)
ProgramStringAuszuführendes Programm
ProgramArgumentsArrayProgramm + Argumente

Zeitsteuerung:

SchlüsselTypBeschreibung
StartIntervalIntegerIntervall in Sekunden
StartCalendarIntervalDict/ArrayKalenderbasierte Ausführung

Auslöser:

SchlüsselTypBeschreibung
RunAtLoadBooleanBei Laden ausführen
WatchPathsArrayBei Dateiänderung ausführen
QueueDirectoriesArrayAusführen wenn Dateien im Ordner
StartOnMountBooleanBei Volume-Mount ausführen

Umgebung:

SchlüsselTypBeschreibung
WorkingDirectoryStringArbeitsverzeichnis
EnvironmentVariablesDictUmgebungsvariablen
UserNameStringBenutzer (nur Daemons)
GroupNameStringGruppe (nur Daemons)

Ausgabe:

SchlüsselTypBeschreibung
StandardOutPathStringStdout in Datei
StandardErrorPathStringStderr in Datei

Verhalten:

SchlüsselTypBeschreibung
KeepAliveBoolean/DictProzess am Leben halten
ThrottleIntervalIntegerMindestzeit zwischen Neustarts
DisabledBooleanJob deaktivieren

4.3 StartCalendarInterval

Ähnlich wie cron-Syntax, aber als Dictionary:

SchlüsselWertebereich
Minute0–59
Hour0–23
Day1–31
Weekday0–7 (0 und 7 = Sonntag)
Month1–12

Beispiele:

<!-- Täglich um 6:30 -->
<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>6</integer>
    <key>Minute</key>
    <integer>30</integer>
</dict>

<!-- Jeden Montag um 9:00 -->
<key>StartCalendarInterval</key>
<dict>
    <key>Weekday</key>
    <integer>1</integer>
    <key>Hour</key>
    <integer>9</integer>
    <key>Minute</key>
    <integer>0</integer>
</dict>

<!-- Mehrere Zeitpunkte (Array) -->
<key>StartCalendarInterval</key>
<array>
    <dict>
        <key>Hour</key>
        <integer>9</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <dict>
        <key>Hour</key>
        <integer>17</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
</array>

4.4 Vollständige Beispiele

Beispiel 1: Tägliches Backup um 2:00 Uhr

Datei: ~/Library/LaunchAgents/com.user.backup.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>com.user.backup</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/max/scripts/backup.sh</string>
    </array>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>2</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>

    <key>StandardOutPath</key>
    <string>/Users/max/logs/backup.log</string>

    <key>StandardErrorPath</key>
    <string>/Users/max/logs/backup-error.log</string>

    <key>WorkingDirectory</key>
    <string>/Users/max</string>

    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    </dict>
</dict>
</plist>

Beispiel 2: Alle 30 Minuten ausführen

Datei: ~/Library/LaunchAgents/com.user.sync.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>com.user.sync</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/max/scripts/sync.sh</string>
    </array>

    <key>StartInterval</key>
    <integer>1800</integer>  <!-- 30 Minuten = 1800 Sekunden -->

    <key>RunAtLoad</key>
    <true/>

    <key>StandardOutPath</key>
    <string>/Users/max/logs/sync.log</string>

    <key>StandardErrorPath</key>
    <string>/Users/max/logs/sync.log</string>
</dict>
</plist>

Beispiel 3: Bei Dateiänderung ausführen (WatchPaths)

Datei: ~/Library/LaunchAgents/com.user.watcher.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>com.user.watcher</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/max/scripts/process-downloads.sh</string>
    </array>

    <key>WatchPaths</key>
    <array>
        <string>/Users/max/Downloads</string>
    </array>

    <key>StandardOutPath</key>
    <string>/Users/max/logs/watcher.log</string>

    <key>StandardErrorPath</key>
    <string>/Users/max/logs/watcher.log</string>
</dict>
</plist>

Beispiel 4: Bei Login ausführen

Datei: ~/Library/LaunchAgents/com.user.startup.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>com.user.startup</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/max/scripts/startup.sh</string>
    </array>

    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

Beispiel 5: Dienst dauerhaft laufen lassen (KeepAlive)

Datei: ~/Library/LaunchAgents/com.user.server.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>com.user.server</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/max/scripts/server.sh</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>ThrottleInterval</key>
    <integer>10</integer>

    <key>StandardOutPath</key>
    <string>/Users/max/logs/server.log</string>

    <key>StandardErrorPath</key>
    <string>/Users/max/logs/server.log</string>
</dict>
</plist>

4.5 Job aktivieren

# 1. Datei erstellen/bearbeiten
nano ~/Library/LaunchAgents/com.user.backup.plist

# 2. Syntax prüfen
plutil -lint ~/Library/LaunchAgents/com.user.backup.plist

# 3. Job laden
launchctl load ~/Library/LaunchAgents/com.user.backup.plist

# 4. Prüfen ob geladen
launchctl list | grep com.user.backup

# 5. Manuell testen
launchctl start com.user.backup

4.6 Job deaktivieren

# Job entladen
launchctl unload ~/Library/LaunchAgents/com.user.backup.plist

# Job deaktivieren (bleibt entladen nach Neustart)
launchctl unload -w ~/Library/LaunchAgents/com.user.backup.plist

# Oder: Disabled-Key in plist setzen

4.7 Fehlersuche

# plist-Syntax prüfen
plutil -lint ~/Library/LaunchAgents/com.user.task.plist

# Geladene Jobs anzeigen
launchctl list | grep com.user

# Exit-Status prüfen (0 = OK, sonst Fehler)
launchctl list | grep com.user.task
# Ausgabe: PID  Status  Label
#          -    0       com.user.task  (Status 0 = OK)
#          -    1       com.user.task  (Status 1 = Fehler)

# System-Log prüfen
log show --predicate 'subsystem == "com.apple.xpc.launchd"' --last 1h | grep com.user

# Ausgabe-Logs prüfen
tail -f ~/logs/task.log

Häufige Fehler:

StatusBedeutung
0Erfolgreich beendet
1Allgemeiner Fehler
78Konfigurationsfehler
126Befehl nicht ausführbar
127Befehl nicht gefunden

5 Unterschiede Cron vs. launchd

5.1 Vergleichstabelle

Merkmalcronlaunchd
HerkunftUnix (1970er)Apple (2005)
KonfigurationTextdatei (crontab)XML (.plist)
SyntaxEinfach, kompaktVerbose, aber flexibel
ZeitsteuerungMinutengenauSekunden möglich
IntervalleNur über ZeitpunkteNative Intervall-Unterstützung
AuslöserNur ZeitZeit, Dateien, Netzwerk, etc.
Verpasste JobsWerden übersprungenWerden nachgeholt
UmgebungMinimalKonfigurierbar
LoggingManuellIntegriert
Dienst-ManagementNicht möglichVollständig (KeepAlive)
macOS-IntegrationBasicVollständig
PortabilitätHoch (alle Unix)Nur macOS

5.2 Wann cron verwenden?

cron ist besser für:

  • Einfache, zeitbasierte Aufgaben
  • Portabilität (Skripte auch auf Linux nutzbar)
  • Schnelle, unkomplizierte Einrichtung
  • Erfahrene Unix-Nutzer

Beispiel-Anwendungsfälle:

# Einfaches tägliches Backup
0 2 * * * /Users/max/scripts/backup.sh

# Stündlicher Check
0 * * * * /Users/max/scripts/check.sh

5.3 Wann launchd verwenden?

launchd ist besser für:

  • macOS-spezifische Aufgaben
  • Reaktion auf Ereignisse (Dateiänderungen, Netzwerk)
  • Dienste die dauerhaft laufen sollen
  • Bessere Fehlerbehandlung
  • Integration mit macOS-Funktionen (Power Management, etc.)
  • Wenn verpasste Jobs nachgeholt werden sollen

Beispiel-Anwendungsfälle:

  • Ordner überwachen und bei Änderung reagieren
  • Dienst der nach Absturz neu startet
  • Task der nur bei Netzwerkverbindung läuft

5.4 Entscheidungshilfe

Aufgabe                                 → Wahl
────────────────────────────────────────────────
Einfache Zeitsteuerung                 → cron
Reaktion auf Dateiänderungen           → launchd
Dauerlaufenden Dienst                  → launchd
Portabilität zu Linux                  → cron
Nachholung verpasster Jobs             → launchd
Schnelle Einrichtung                   → cron
macOS-Integration (Sleep/Wake)         → launchd
Komplexe Bedingungen                   → launchd

5.5 Migration cron → launchd

cron:

30 2 * * * /Users/max/scripts/backup.sh

Äquivalentes launchd:

<?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>com.user.backup</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Users/max/scripts/backup.sh</string>
    </array>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>2</integer>
        <key>Minute</key>
        <integer>30</integer>
    </dict>
</dict>
</plist>

5.6 Beide parallel nutzen

Es ist möglich, beide Systeme gleichzeitig zu verwenden:

  • cron für einfache, portable Aufgaben
  • launchd für macOS-spezifische Anforderungen

Tipp: Dokumentieren, welche Aufgaben wo konfiguriert sind:

# Alle aktiven Jobs anzeigen
echo "=== Cron Jobs ==="
crontab -l

echo ""
echo "=== LaunchAgents ==="
ls ~/Library/LaunchAgents/

echo ""
echo "=== Geladene Jobs ==="
launchctl list | grep com.user

5.7 Empfehlung

Für neue macOS-Projekte wird launchd empfohlen:

  1. Bessere Integration ins System
  2. Mehr Flexibilität bei Auslösern
  3. Zuverlässigere Ausführung
  4. Bessere Logging-Möglichkeiten
  5. Von Apple unterstützt und gepflegt

cron bleibt eine gute Wahl für:

  1. Einfache, zeitbasierte Tasks
  2. Cross-Platform-Kompatibilität
  3. Schnelle Einrichtung ohne XML