Here is something that has come in handy for me a handful of times in the past 6 months. Say you are running a Rails app, and you have a rake task that you’d like to run on a recurring basis. On a *nix based web server you would probably jump straight to the crontab - but what if your server is a Mac?
While OS X supports cron, the preferred way to handle such tasks is via Launch Daemons. So how does one do this exactly?
Finding Your Copy of Rake
Before we can go setting up any daemons, we will need to know where your copy of rake resides.
RVM Users: Create a wrapper executable using the rvm wrapper command. Then type which wrappername_rake to get the path to your wrapper.
gregs-macbook:undefined_behavior gregwoods$ rvm wrapper 1.9.3 launchctl
Saving wrappers to '/Users/gregwoods/.rvm/bin'.
gregs-macbook:undefined_behavior gregwoods$ which launchctl_rake
/Users/gregwoods/.rvm/bin/launchctl_rake
The wrapper does two things: It loads up your RVM environment, and then it calls to the actual rake exectutable wherever it happens to be installed.
Everybody Else: Simply type which rake and take note of the path. Done!
Create Your Plist
Create an empty text file under /Library/LaunchDaemons, and name it using the reverse domain naming convention with a .plist extension. The filename should roughly correlate to the application and task at hand.
In a recent example, I had a rake task that was in charge of downloading sales data from a remote server and importing it into a mysql database. So I’ll call my file net.undefinedbehavior.my_app.sales.plist.
Launch Daemon plists are a simple list of configuration key/value pairs in XML format. The available options can be found on this man page. Here is what my file looks like before adding much configuration - note how the the Label value matches the file name minus the extension.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>net.undefinedbehavior.my_app.sales</string>
</dict>
</plist>
Now let’s start filling in the values. First and perhaps most importantly, ProgramArguments defines a string of shell commands used to execute the rake task. Arguments are passed in as a array; As a general rule of thumb, every block of text seperated by a space should be entered as an item in the array. I’m using the path to rake here that I looked up earlier.
<key>ProgramArguments</key>
<array>
<string>/Users/gregwoods/.rvm/bin/launchctl_rake</string>
<string>-f</string>
<string>/srv/sites/myapp.undefinedbehavior.net/Rakefile</string>
<string>myapp:sales:refresh</string>
</array>
For reference, the above would translate into /Users/gregwoods/.rvm/bin/launchctl_rake -f /srv/sites/myapp.undefinedbehavior.net/Rakefile myapp:sales:refresh on the command line. With me so far?
Now I’d like to be able to track the output of this task, so I’m going to define a standard output and standard error log. For convenience I’ll go ahead and send both to the same file, which I’ll place in my applications /log directory.
<key>StandardOutPath</key>
<string>/srv/sites/myapp.undefinedbehavior.net/log/launchctl.log</string>
<key>StandardErrorPath</key>
<string>/srv/sites/myapp.undefinedbehavior.net/log/launchctl.log</string>
And now I’d like to schedule the job to happen at the start of every hour.
<key>StartCalendarInterval</key>
<dict>
<key>Minute</key>
<integer>0</integer>
</dict>
Times segments not included in the interval are considered to be wild cards. The fact that I left out the Hour key/value means this job will run every hour on the given minute.
Finally we have the full configuration, with a few other minor options tossed in for good measure. I would highly encourage you to look up the remaining configs in the man page to find out what they do.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>net.undefinedbehavior.my_app.sales</string>
<key>UserName</key>
<string>deploy</string>
<key>ProgramArguments</key>
<array>
<string>/Users/gregwoods/.rvm/bin/launchctl_rake</string>
<string>-f</string>
<string>/srv/sites/myapp.undefinedbehavior.net/Rakefile</string>
<string>myapp:sales:refresh</string>
</array>
<key>StandardOutPath</key>
<string>/srv/sites/myapp.undefinedbehavior.net/log/launchctl.log</string>
<key>StandardErrorPath</key>
<string>/srv/sites/myapp.undefinedbehavior.net/log/launchctl.log</string>
<key>KeepAlive</key>
<false/>
<key>debug</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>OnDemand</key>
<true/>
<key>StartCalendarInterval</key>
<dict>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>Disabled</key>
<false/>
</dict>
</plist>
Starting and Stopping Your Daemon
Now for the final step - loading the job! At your terminal enter the following command:
sudo launchctl load /Library/LaunchDaemons/net.undefinedbehavior.my_app.sales.plist
A few more useful commands:
# Unload the daemon:
sudo launchctl unload /Library/LaunchDaemons/net.undefinedbehavior.my_app.sales.plist
# Stop the daemon without unloading it
sudo launchctl stop net.undefinedbehavior.my_app.sales
# Start the daemon if it is stopped
sudo launchctl start net.undefinedbehavior.my_app.sales
# Find loaded daemons, view the most recent exit code for your daemon
sudo launchctl status
Try launchctl help for more useful options. And if you’re having trouble with a particular job, try tailing /var/log/system.log for relevant crash reports.
More Examples
Here is one more example launch daemon, this one designed to trigger a reindex of Thinking Sphinx every night at midnight.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>net.undefinedbehavior.my_app.reindex</string>
<key>UserName</key>
<string>deploy</string>
<key>ProgramArguments</key>
<array>
<string>/Users/gregwoods/.rvm/bin/launchctl_rake</string>
<string>-f</string>
<string>/srv/sites/myapp.undefinedbehavior.net/Rakefile</string>
<string>ts:reindex</string>
</array>
<key>KeepAlive</key>
<false/>
<key>debug</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>OnDemand</key>
<true/>
<dict>
<key>Minute</key>
<integer>0</integer>
<key>Hour</key>
<integer>0</integer>
</dict>
<key>Disabled</key>
<false/>
</dict>
</plist>
Now go forth, and launch those daemons!