• Mac
  • 9. marts 2026

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/LaunchAgents versus /Library/LaunchDaemons.
  • Den rigtige syntaks for nøgler som StartInterval, StartCalendarInterval og KeepAlive.
  • 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. 900 betyder “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 eksekverings­rettigheder: chmod 755 /usr/local/bin/backup_home.sh
  • Test manuelt fra Terminal, før du laver plist’en.

  • Brug omvendt domænenavn som prefix: com.firmanavn.backup-home.plist.
  • Bruger­specifik 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.plist

Systemjob (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.plist

5. 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-home

Se loggen:

tail -f /tmp/backup_home.outtail -f /tmp/backup_home.err

6. Hurtig tjekliste (undgå klassiske faldgruber)

  • 100 % fulde stier i både plist og script.
  • Rigtige ejere/permissions på både script og plist.
  • ProgramArguments foretrækkes frem for Program.
  • Ret PATH via EnvironmentVariables - 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.plist

Er 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 syslog

Afslutter 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/LaunchAgents skal ejes af brugeren og må ikke være world-skrivbar (<= 644).
  • Program vs. ProgramArguments: Brug ProgramArguments (array) i 99 % af tilfældene. Program bruges kun til binære uden argumenter.
  • PATH og andre variabler: launchd arver ikke din shell-profil. Definér EnvironmentVariables eller brug absolutte stier (anbefales).
  • Ugyldige StartCalendarInterval: Glemte nul-prefikser (2 vs 02) 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/OtherJobEnabled eller 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/rsync frem for rsync) og fang fejltilstande med eksplicit exit-kode.
  • Log til dedikerede filer og roter dem med newsyslog eller logrotate for at undgå voksende logs.
  • Versionskontrollér både script og .plist i Git; små ændringer i StartCalendarInterval m.m. bliver hurtigt overset uden diff-historik.
  • Ved afinstallation: launchctl bootout (system) eller launchctl 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.