Launchctl vs Cron: You’ve Got To Be Kidding Me

So I wanted to write a cron job on my Mac. Just run this script every day at midnight. Nothing fancy.

In standard Unix, this is one line in a crontab:

0 0 * * * /some/script

That’s it. Put that in your crontab and /some/script will run every day at midnight.

Naturally, I typed man crontab on my Mac to get started. Apple likes to make weird minor modifications to standard Unix commands sometimes. I found:

(Darwin note: Although cron(8) and crontab(5) are officially supported under Darwin, their functionality has been absorbed into launchd(8), which provides a more flexible way of automatically executing commands. See launchctl(1) for more information.)

More flexible, huh? I’m all about more flexible! Sounds good. Another well thought out Apple improvement, sweeping away 30+ years of Unix cruft at a stroke!

Not quite. Turns out that you must submit jobs to launchctl as XML files. We all know how much I hate XML files, and this is a great example of why. That one line cronjob becomes this fragile ~20 line XML monstrosity:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "">
<plist version="1.0">
<string>Updates statistics for every day at 3AM.</string>

But it defines its own syntax!! I can already hear the XML cheerleaders say. Well, sort of. You can read it, kinda, even if you are unfamiliar with the syntax, but you certainly can’t write it without spending half an hour or more reading about and then absorbing the syntax documentation first. For comparison, you can learn the syntax of a crontab entry in around 1 minute. Hell, the documentation of the crontab syntax is shorter than that one simple launchd plist example, nevermind the 13 page manpage explaining the launchd plist syntax.

More verbosity means more things that can be screwed up, as that discussion thread amply demonstrates. Even extremely simple jobs become ordeals. In this case, the problem stems from trying to do too much in one tool, forgetting the Unix philosophy: do one thing, and do it well. Create reusable, chainable tools, rather than one monolithic beast that tries to do everything. Apple bills launchd as a replacement for init, rc, init.d, rc.d, SystemStarter, inetd/xinetd, atd, crond, and watchdogd. Maybe others, too.

Developing a language powerful enough to capture the functionality of all of those different tools results in a massively complex, fragile, general-purpose language. Developing a tool that does exactly 1 of those things results in a tiny, simple, clean, resilient language that is easy to use and hard to screw up. Complex cases are then handled by gluing those many small tools together, not by programming a massive monolith.

Fortunately, cron is still supported by Mac OS X as a legacy service, so I’m using that. If and when they finally remove it from the OS altogether, I’ll be using the MacPorts version.

3 Responses to Launchctl vs Cron: You’ve Got To Be Kidding Me

  1. Unix is definitely the hell living beneath the solid, happy surface of the Mac land.

    It’s good and strong — but daemons live there — as I am sure you are aware.

    Yes, those plists are shocking. I wonder if I will get all my crons converted.

    Unfortuntely, around the 10.4 timeframe, cron started working not so well — sometime it come up resource starved (“[1] (0x10fe60.cron[22361]): Could not setup Mach task special port 9: (os/kern) no access” is typical).

    So I guess we are all stuck with the all singing & dancing launchd.

  2. I agree with both of you. Launchd is too big and too complex for simple tasks. I’ve been writing a ‘batch’ wrapper for it as I try to migrate to it. But it is a pain for us old school UNIX guys.

  3. Well, let me correct myself…

    RTFM — or more accurately, studying it carefully can pay off. While it is true you can create plist files to run a job and have a lot of fine control over it, there is a simpler option that works well. The launchctl command also has a ‘submit’ option whereby you can start a job with one command like this:

    launchctl submit -l task1 — $HOME/bin/ foo bar

    This will create a background task without the need to create any plist files. The default mode of this kind of job is to run all the time, so you should also learn that to stop the job you need to run:

    launchctl remove task1

    This “discovery” has caused me to appreciate this new development in submitting and managing background jobs. There are still pros and cons and things you need to be aware of (RTFM!), but overall it is a very useful tool for quickly submitting jobs to run under launchd.