Bad Ideas: Cron Replacement on OSX 10.11
OSX 10.11, aka El Capitan, was released a few days ago and introduced System Integrity Protection (SIP). SIP is a security mechanism which, at its most basic, prevents the root user from performing all sorts of actions. It's conceptually very similar to SELinux but with none of the configuration ability. For the purposes of this post, the key "issue" is that SIP prevents root from writing to protected directories, namely /usr (amongst others).
In setting up a new machine tonight, I ran into an issue. I was unable to create a new crontab. After some digging, I realized that all of cron's magic is in /usr/lib on OSX. SIP is guarding /usr now so there's no way to get a new crontab going. That's … problematic.
Years ago, Apple transitioned all of its tooling away from cron into launchd. Cron-like functions are configured via plist files in ~/Library/LaunchAgents or /Library/LaunchAgents (for apps to be executed as root). The syntax is clunky as all hell but the system works. The "right way" would be to rewrite my desired crontab entries into individual launchd plists. That sounds really boring. Besides, why do things the "right" way when I could do something terrible instead?
I decided to build a replacement for cron via launchd and some shell. It ended up working like /etc/cron.hourly and /etc/cron.daily that anacron usually provides.
Here's the launchd file for the 5 minute execution. This lives in ~/Library/LaunchAgents/com.sungo.fivemincron.plist. Load it up by running launchctl load com.sungo.fivemincron.plist. If you are a tmux user, you must run this command outside of tmux or you'll get a permission denied error.
<?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>com.sungo.fivemincron</string> <key>ProgramArguments</key> <array> <string>/Users/sungo/bin/five_minute_cron</string> </array> <key>Nice</key> <integer>1</integer> <key>StartInterval</key> <integer>300</integer> <key>RunAtLoad</key> <true/> <key>StandardErrorPath</key> <string>/Users/sungo/log/fivemin-err.log</string> <key>StandardOutPath</key> <string>/Users/sungo/log/fivemin-out.log</string> </dict> </plist>
Hopefully it's obvious but that runs /Users/sungo/bin/five_minute_cron every 300 seconds. For a normal person, you can probably stop here. Put whatever code you want to run in-series in that script and you're good.
I'm not normal.
With cron, if I set several commands to run every five minutes, they run in parallel. Putting those commands into a script gets them to run in series which is not what I want.
gnu-parallel to the rescue.
Here's what five_minute_cron looks like with a dash of crazy.
#!/usr/local/bin/parallel --shebang --will-cite ::: /Users/sungo/bin/script_one /Users/sungo/bin/script_two
-shebang tells parallel that the following lines make up its config file. -will-cite gets rid of the really fucking annoying plea for citations. (Yeah, I don't even, either.) The ::: tells parallel to use the remaining arguments as command input source rather than waiting for commands via stdin.
All in all, that script runs the commands listed in parallel. Launchd will run the script every 300 seconds.
So now I have a cron-ish replacement using launchd and gnu parallel.