Moving from cron to launchd on Mac OS X Server

As the usage of cron is now deprecated on OS X and OS X Server in favour of launchd, I thought it was about time I learnt how to use launchd so I could move all my cronjobs across to it. launchd is one of Apple’s various contributions to the Unix world, and its purpose is to be a single tool to take the place of init and to replace a variety of startup mechanisms such as the rc/rc.d startup architecture, cron, and inetd/xinetd. In keeping with most other technologies emanating from Apple it is simple, elegant, efficient and powerful. If you do a significant amount of Mac administration then now is the time to learn launchd if you haven’t already.

However… it’s a bit of a hassle to learn launchd and Apple’s XML property list (‘plist’) files when you were comfortable with cron and it was doing the job fine before. But I found that launchd-related stuff is pretty well documented and quite straightforward once you get going with it. Here’s an example of a plist file I created which triggers a script that runs a backup job then emails me the output:

<?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>svn_sync</string>
  <key>UserName</key>
  <string>dave</string>
  <key>Program</key>
  <string>/usr/local/bin/svn_sync.sh</string>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Minute</key>
    <integer>8</integer>
    <key>Hour</key>
    <integer>8</integer>
  </dict>
  <key>Debug</key>
  <false/>
  <key>AbandonProcessGroup</key>
  <true/>
</dict>
</plist>

This is a pretty standard plist file, and the plist files you create for launchd are generally going to follow this sort of layout. The pertinent bits of it are as follows:

  <key>Label</key>
  <string>svn_sync</string>

This is the name of the job, in this case ‘svn_sync’.

  <key>UserName</key>
  <string>dave</string>

This specifies the user to run the script as (‘dave’ in this case). No need to mess about with ‘su’ stuff.

  <key>Program</key>
  <string>/usr/local/bin/svn_sync.sh</string>

This is the script (or other type of program) you want to run (‘svn_sync.sh’ in this case), with its full path.

  <key>StartCalendarInterval</key>
  <dict>
    <key>Minute</key>
    <integer>8</integer>
    <key>Hour</key>
    <integer>8</integer>
  </dict>

This gives the timings of when the script should run, and it works very much like the first five fields in a standard crontab. In this case the script runs every day at 8:08 in the morning.

  <key>Debug</key>
  <false/>

Debugging is set to ‘off’, but it can easily be turned on to debug problems. Output from launchd appears in /var/log/system.log and is not always as helpful as it could be, but it’s certainly better than nothing.

  <key>AbandonProcessGroup</key>
  <true/>

For a while I couldn’t work out why the backup script was being triggered by launchd but was failing to email me its output. This error message appeared in the system log each time:

(svn_sync[91222]): Stray process with PGID equal to this dead job: PID 91373 PPID 1 sendmail

After a while I learnt that when launchd had finished running the backup script, it was also killing all processes started from within the script – in this case the process of mailing me the results of the backup. Setting ‘AbandonProcessGroup’ to true tells launchd to leave all child processes running rather than killing them. It’s quite an important thing to know about, really, otherwise it becomes an irritating mystery as to why bits of your script are not getting finished.

I called my plist file svn_sync.plist and put it in /Library/LaunchDaemons. With plist files for launchd it appears you basically have a choice between putting them in /Library/LaunchDaemons or /Library/LaunchAgents. Apple’s Daemonomicon (what a lovely word!) explains all of this in detail, but basically, if I’ve understood correctly, the plist file should go into /Library/LaunchAgents if it might interact with a user whilst it runs, otherwise it should go into /Library/LaunchDaemons. The latter seemed more appropriate in this case because the script will never interact directly with a user.

Once your launchd plist file is in place, just use launchctl to load it into launchd:

launchctl load /Library/LaunchDaemons/svn_sync.plist

And that should be it.