Using launchctl as an anacron replacement

November 28, 2015

I have a lot of scripts that automate the boring parts of my life. Recently an article was posted about one system administrator who automated everything down to scripting the coffee maker to produce cups of coffee for him. I don't go quite that far with my automation, but it inspired me to write about a lesser known tool in OSX - launchd and its interfacing agent: launchctl.

You may be familiar with launchctl if you've ever run a database or cache server on a Mac locally. Or, you may know it indirectly through the use of a wrapper called lunchy, which makes working with launchctl a much more sane experience.

At a high level, launchctl aims to make it easy to run daemonized processes on Mac. It can manage these processes for a user or for the system as a whole. Many of the services on Mac that you interface with every day are managed by launchd such as Spotlight and AirPlay.

To explore the world of launchd and launchctl, I'm going to examine a specific use case I had for using the service: updating brew. I have a bad habit of forgetting to update my brew formulas for weeks (sometimes months) at a time. The reminder usually comes in the form of an error when trying to install a formula that mysteriously doesn't exist. I thought: "This is a perfect use case for cron!" and I was probably right except for the minor (read: show-stopping) issue of being on a laptop.

I needed a way to be able to update my brew formulas automatically in the background on a rough schedule. If my laptop was off when the update was scheduled to run, I would expect it to run the next time I turned on my laptop. On a more fine level, I would like to retry the update if it failed and I would like the output to be logged to a file in order to check on the status of previous runs. Enter launchd.

Every launchd service is configured using a plist file. Multiple options can be specified in the plist file. The documentation for those options can be found in the man pages (gasp!) under the launchd.plist entry. These configuration files usually live in ~/Library/LaunchAgents. If your user has no custom services yet running via launchd, you will need to create that folder. A benefit of this is that files for all of the daemons live in one location, which makes it easy to organize and keep track of the different services.

Generally, the configuration files are named using a combination of your domain and the name of the service. I decided to be creative and call this service brewd. Below is the plist file that satisfied my requirements located at ~/Library/LaunchAgents/me.jcomo.brewd.plist.

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" ""> <plist version="1.0"> <dict> <key>Label</key> <string>me.jcomo.brewd</string> <key>KeepAlive</key> <dict> <key>SuccessfulExit</key> <false /> </dict> <key>ThrottleInterval</key> <integer>60</integer> <key>StartCalendarInterval</key> <dict> <key>Minute</key> <integer>0</integer> <key>Hour</key> <integer>10</integer> <key>Weekday</key> <integer>2</integer> </dict> <key>ProgramArguments</key> <array> <string>/usr/local/bin/brew</string> <string>update</string> </array> <key>StandardOutPath</key> <string>/usr/local/var/brewd/output.log</string> <key>StandardErrorPath</key> <string>/usr/local/var/brewd/error.log</string> </dict> </plist>

I'm not going to go into too much depth on each of the options since the descriptions and parameters can all be found in the man page. However, the crucial part in this file, and the point of this post, is under the key StartCalendarInterval. Here, you can define a cron-like schedule to run the service. According to the docs,

Unlike cron which skips job invocations when the computer is asleep, launchd will start the job the next time the computer wakes up. If multiple intervals transpire before the computer is woken, those events will be coalesced into one event upon wake from sleep.

Bingo! The other options in the configuration specify where to direct stderr and stdout, how often to retry if the program fails, and finally, the actual command that should be scheduled and run.

The last step is to make launchd aware of this configuration. To do that, launchctl must be used to load the plist. This can be done by running launchctl load ~/Library/LaunchAgents/me.jcomo.brewd.plist. If I no longer want a service to be running, I can simply unload it.

Now my brew formulas will be updated approximately every Monday morning at 10am. The emphasis is on approximately; this would be a bad replacement for cron for a task that needed to run on a strict schedule.

There is a lot more depth to launchd and it fits more use cases than the one presented in this post. For more information, I'd encourage exploring the man pages for both launchd.plist and launchctl as it was hard to find specific help online.

Disclaimer: there are likely other solutions to this problem. While launchd and launchctl certainly have their quirks, I find them to be a great solution for running daemon processes on my laptop.