Konfigurer cron-lignende job med launchd og plist-filer
Har du stadig en gammel crontab liggende på din Mac? Så er du ikke alene
Mange udviklere, systemadministratorer - og ja, også den ivrige hobby‐automatiseringsnørd - opdager før eller siden, at deres velkendte cron‐jobs enten kører ustabilt eller helt er holdt op med at virke efter en macOS‐opgradering. Forklaringen er enkel: cron er for længst blevet degraderet til andenviolin, mens launchd nu dirigerer hele showet omkring tidsplanlagte opgaver.
I denne artikel dykker vi ned i, hvordan du får samme (og mere) funktionalitet som cron - bare på den moderne macOS‐måde. Vi tager dig fra de helt basale begreber som LaunchAgents, LaunchDaemons og plist‐filer til konkrete eksempler, der kan køre alt fra et simpelt backup‐script hver nat kl. 02:00 til et sundhedstjek hvert 15. minut.
Undervejs lærer du:
- Hvorfor og hvornår du bør placere dine job i
~/Library/LaunchAgentsversus/Library/LaunchDaemons. - Den rigtige syntaks for nøgler som
StartInterval,StartCalendarIntervalogKeepAlive. - Praktiske gotchas om PATH‐variabler, filrettigheder og log‐rotation, som sparer dig timers fejlfinding.
Lyder det som noget for dig? Så læn dig tilbage, åbn Terminal, og lad os gøre dine automatiseringer på macOS både robuste og fremtidssikrede med launchd.
Overblik: launchd som macOS’ erstatning for cron
Hvis du har rodet med Unix-systemer i årevis, er cron sikkert din trofaste ven. På macOS er den dog reelt sat på pension. Siden 10.4 Tiger har Apple i stedet leveret launchd - en init-lignende tjeneste, som både håndterer opstart af systemprocesser og planlægning af tilbagevendende jobs. Fordelen er, at ét dæmon-framework kan:
- Afvikle processer fra boot til bruger-login.
- Starte jobs baseret på tid, system-hændelser (sleep/wake, netværk, filsystem) eller når en filsocket bliver adresseret.
- Sikre korrekt dependency-håndtering og automatisk genstart (KeepAlive) ved nedbrud.
Vigtige begreber
- Job - den enkelte konfiguration (en plist-fil) som fortæller launchd hvad der skal køres, hvordan og hvornår.
- LaunchAgent - et job der kører i bruger-kontekst, kun når den pågældende bruger er logget ind (har en grafisk session).
- LaunchDaemon - et job der kører i system-kontekst, tilgængeligt allerede på early-boot, uafhængigt af logins.
- plist-fil - job-definitionen skrevet i XML (Property List). macOS læser dem automatisk, når de ligger de rigtige steder på disken med korrekte rettigheder.
Hvor hører filerne hjemme?
-
~/Library/LaunchAgents- brugerens egne jobs. Ejerskab: den enkelte bruger. Indlæses ved login. Velegnet til personlige scripts, som skal have adgang til brugerens GUI og Keychain. -
/Library/LaunchAgents- globale bruger-jobs. Ejerskab:root:wheel, men kører stadig som hver bruger der logger ind. Typisk brugt af tredjeparts-apps med menulinje-helper. -
/Library/LaunchDaemons- system-jobs. Ejerskab:root:wheel. Kører selv før login, uden GUI. Perfekt til server-processer, backup-scripts og lignende.
Når du vælger Agent kontra Daemon, skal du altså afgøre:
- Behøver processen skrive til brugerens skærm eller interagere med GUI?
- Skal den køre, selv hvis ingen er logget ind?
- Hvilke rettigheder (filer, netværk, Keychain) har den brug for?
Tidsplanlægning i launchd
Hvor cron benytter det klassiske * * * * *-format, arbejder launchd med deklarative nøgler i plist-filen:
-
StartInterval - heltal i sekunder.
900betyder “kør hvert 15. minut”. -
StartCalendarInterval - et dictionary (eller array af dictionaries) med felter som
Minute,Hour,Weekday,Day,Month. F.eks.<dict><key>Hour</key><integer>2</integer><key>Minute</key><integer>0</integer></dict>for dagligt kl. 02:00. -
RunAtLoad -
true/false. Kør jobbet straks det indlæses (god til initialisering eller når maskinen vågner). -
KeepAlive - holder processen i live. Kan være
true(automatisk genstart) eller et mere detaljeret dictionary (fx<key>SuccessfulExit</key><false/>).
Kombinationen af disse felter gør timing mere fleksibel end cron - og launchd håndterer desuden maskinens sleep/vågne cyklus: et job, der var planlagt under sleep, vil som udgangspunkt køre umiddelbart efter wake.
Ved at forstå forskellene mellem LaunchAgents og LaunchDaemons, samt de centrale tids-nøgler, kan du nemt migrere eksisterende cron-tasks - eller designe helt nye workflows - på en måde, der passer perfekt til macOS’ moderne arkitektur.
Sådan bygger du en plist og planlægger et job (trin for trin)
Nedenfor følger en komplet, praktisk gennemgang af, hvordan du går fra et løst shell-script til et fuldt fungerende launchd-job, der kan erstatte dine gamle cron-linjer.
1. Skriv (og test) selve scriptet
#!/bin/bash# /usr/local/bin/backup_home.shset -euo pipefailSOURCE="$HOME/Documents"DEST="/Volumes/BackupDisk/Documents_$(date +%Y-%m-%d).tar.gz"tar -czf "$DEST" "$SOURCE"- Sørg altid for absolutte stier. $PATH er ikke garanteret i
launchd-kontekst. - Giv scriptet eksekveringsrettigheder:
chmod 755 /usr/local/bin/backup_home.sh - Test manuelt fra Terminal, før du laver plist’en.
2. Navngivning og placering af plist-filen
- Brug omvendt domænenavn som prefix:
com.firmanavn.backup-home.plist. - Brugerspecifik opgave:
~/Library/LaunchAgents/
System- eller root-opgave:/Library/LaunchDaemons/. - Ejerskab og rettigheder:
LaunchAgents: bruger:staff, 644
LaunchDaemons: root:wheel, 644
3. Byg plist’en - Kernefelter forklaret
<?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> <!-- 1. Unik identifikator --> <key>Label</key> <string>com.firmanavn.backup-home</string> <!-- 2. Kommando + argumenter som et array --> <key>ProgramArguments</key> <array> <string>/usr/local/bin/backup_home.sh</string> </array> <!-- 3. Hvor køres vi fra? --> <key>WorkingDirectory</key> <string>/usr/local/bin</string> <!-- 4. Evt. miljøvariabler --> <key>EnvironmentVariables</key> <dict> <key>PATH</key> <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string> </dict> <!-- 5. Hvor gemmes output? --> <key>StandardOutPath</key> <string>/tmp/backup_home.out</string> <key>StandardErrorPath</key> <string>/tmp/backup_home.err</string> <!-- 6. Planlægning --> <!-- Variant A: hvert 15. minut, cron: */15 * * * * --> <key>StartInterval</key> <integer>900</integer> <!-- Variant B (fjern StartInterval hvis du bruger denne): dagligt kl. 02:00, cron: 0 2 * * * --> <!-- <key>StartCalendarInterval</key> <dict> <key>Hour</key><integer>2</integer> <key>Minute</key><integer>0</integer> </dict> --> <!-- 7. Ekstra flag --> <key>RunAtLoad</key><true/> <!-- kør når plist’en indlæses --> <key>KeepAlive</key><false/> <!-- genstart ikke scriptet konstant --> <key>Disabled</key><false/> <!-- sæt til true for at deaktivere --></dict></plist>Mapping af klassiske cron-udtryk
| Cron | launchd |
|---|---|
| */5 * * * * | StartInterval = 300 |
| 0 2 * * * | StartCalendarInterval: {Hour=2; Minute=0} |
| 30 8 * * 1-5 | {Hour=8; Minute=30; Weekday=1-5} |
4. Indlæs og aktiver jobben
Brugerjob (LaunchAgent):
launchctl load ~/Library/LaunchAgents/com.firmanavn.backup-home.plist# Catalina+ (per-user bootstrap namespace):# launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.firmanavn.backup-home.plistSystemjob (LaunchDaemon): kør som root:
sudo launchctl load /Library/LaunchDaemons/com.firmanavn.backup-home.plist# ny syntaks:# sudo launchctl bootstrap system /Library/LaunchDaemons/com.firmanavn.backup-home.plist5. Første testkørsel
# Tving omgående kørsel (macOS 11+):launchctl kickstart -k gui/$(id -u)/com.firmanavn.backup-home# Ældre syntax:launchctl start com.firmanavn.backup-homeSe loggen:
tail -f /tmp/backup_home.outtail -f /tmp/backup_home.err6. Hurtig tjekliste (undgå klassiske faldgruber)
- 100 % fulde stier i både plist og script.
- Rigtige ejere/permissions på både script og plist.
-
ProgramArgumentsforetrækkes frem forProgram. - Ret
PATHviaEnvironmentVariables- antag ikke shell-login-filtrene. - Valider plist:
plutil com.firmanavn.backup-home.plist.
Med ovenstående trin er dit job nu fuldt integreret i macOS’ launchd-økosystem - klar til pålidelig, tidsstyret kørsel uden de begrænsninger, klassisk cron lider under.
Drift, fejlfinding og best practices
launchctl er dit primære værktøj til at inspicere og styre jobs i både bruger- og systemkontekst. De mest brugte kommandoer er:
# Se alle indlæste jobs for din brugerlaunchctl list# Udskriv fuld status + sidste exit-kode for ét joblaunchctl print gui/$(id -u)/com.firmanavn.job# Tving et job til at køre med det sammelaunchctl kickstart -k gui/$(id -u)/com.firmanavn.job# Midlertidigt deaktivér/aktivér et job (uden at fjerne plist-filen)launchctl disable gui/$(id -u)/com.firmanavn.joblaunchctl enable gui/$(id -u)/com.firmanavn.job# Fjern et systemdaemon fra hukommelsen og stoppet detsudo launchctl bootout system /Library/LaunchDaemons/com.firmanavn.job.plistEr du i tvivl om domænet (gui vs system), så brug launchctl print system og launchctl print gui/UID for at lede efter din Label.
Logs og fejlfinding
stdout og stderr kan du med fordel pege til egne filer via StandardOutPath/StandardErrorPath. Derudover logger launchd selv til Unified Logging, som du kan trække ud med:
# Se de seneste 5 minutter for ét joblog show --last 5m --predicate 'process == "com.apple.launchd" && eventMessage CONTAINS "com.firmanavn.job"'# Live-followlog stream --predicate 'process == "com.firmanavn.job"' --style syslogAfslutter jobbet med exit-kode ≠ 0 vil det fremgå af både launchctl print og Unified Logging. Brug det til hurtigt at identificere syntaksfejl, manglende rettigheder eller forkerte sti-angivelser.
Almindelige faldgruber
-
Forkert ejerskab/rettigheder: En plist i
~/Library/LaunchAgentsskal ejes af brugeren og må ikke være world-skrivbar (<= 644). -
Program vs. ProgramArguments: Brug
ProgramArguments(array) i 99 % af tilfældene.Programbruges kun til binære uden argumenter. -
PATH og andre variabler: launchd arver ikke din shell-profil. Definér
EnvironmentVariableseller brug absolutte stier (anbefales). -
Ugyldige
StartCalendarInterval: Glemte nul-prefikser (2vs02) eller ugyldige felter (Minute=60) får jobbet til aldrig at køre — og launchd advarer kun i loggen. -
Søvn/vågne: Som udgangspunkt køres et tidsbaseret job ikke, mens maskinen sover. Tilpas evt. med
KeepAlive+PathState/OtherJobEnabledeller planlæg 'catch up' i selve scriptet.
Best practices for robuste launchd-jobs
- Skriv idempotente scripts (de må køres flere gange uden bivirkninger).
- Angiv fulde binære stier (
/usr/bin/rsyncfrem forrsync) og fang fejltilstande med eksplicit exit-kode. - Log til dedikerede filer og roter dem med
newsyslogellerlogrotatefor at undgå voksende logs. - Versionskontrollér både script og
.plisti Git; små ændringer iStartCalendarIntervalm.m. bliver hurtigt overset uden diff-historik. - Ved afinstallation:
launchctl bootout(system) ellerlaunchctl bootout gui/$(id -u)(bruger), fjern plist-filen og dine logfiler. - Test altid manuelt (
kickstart) før automatisering, og overvåg første eksekvering for at sikre korrekt konfiguration.
Med ovenstående retningslinjer får du et reproducerbart, sikkert og let vedligeholdt alternativ til klassiske cron-jobs på macOS.
Indholdsfortegnelse
- Overblik: launchd som macOS’ erstatning for cron
- Sådan bygger du en plist og planlægger et job (trin for trin)
- 1. Skriv (og test) selve scriptet
- 2. Navngivning og placering af plist-filen
- 3. Byg plist’en - Kernefelter forklaret
- 4. Indlæs og aktiver jobben
- 5. Første testkørsel
- 6. Hurtig tjekliste (undgå klassiske faldgruber)
- Drift, fejlfinding og best practices