stalepretzel stalepretzel - 4 months ago 13
Bash Question

How do I set a task to run every so often?

How do I have a script run every, say 30 minutes? I assume there are different ways for different OSs. I'm using OS X.

Answer

Just use launchd. It is a very powerful launcher system and meanwhile it is the standard launcher system for Mac OS X (current OS X version wouldn't even boot without it). For those who are not familiar with launchd (or with OS X in general), it is like a crossbreed between init, cron, at, SysVinit (init.d), inetd, upstart and systemd. Borrowing concepts of all these projects, yet also offering things you may not find elsewhere.

Every service/task is a file. The location of the file depends on the questions: "When is this service supposed to run?" and "Which privileges will the service need?"

System tasks go to

/Library/LaunchDaemons/

if they shall run, no matter if any user is logged in to the system or not. They will be started with "root" privileges.

If they shall only run if any user is logged in, they go to

/Library/LaunchAgents/

and will be executed with the privileges of the user that just logged in.

If they shall run only if you are logged in, they go to

~/Library/LaunchAgents/

where ~ is your HOME directory. These task will run with your privileges, just as if you had started them yourself by command line or by double clicking a file in Finder.

Note that there also exists /System/Library/LaunchDaemons and /System/Library/LaunchAgents, but as usual, everything under /System is managed by OS X. You shall not place any files there, you shall not change any files there, unless you really know what you are doing. Messing around in the Systems folder can make your system unusable (get it into a state where it will even refuse to boot up again). These are the directories where Apple places the launchd tasks that get your system up and running during boot, automatically start services as required, perform system maintenance tasks, and so on.

Every launchd task there is a file in plist format. It should have reverse domain name notation. E.g. you can name your task

com.example.my-fancy-task.plist

This plist can have various options and settings. Writing one per hand is suboptimal, you may want to get the free tool Lingon to create your tasks. This tool used to be free, now it costs $5 in the app store and $10 as the non app store version (the non app store version is much more powerful and if you already plan on paying for it, seriously, get the non app store version). If anyone knows a comparable tool that is freeware or open source, drop me a line in the comments and I will rather recommend that one (don't want to advertise here for commercial software).

Just as an example, it could look like this

<?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>com.example.my-fancy-task</string>
    <key>OnDemand</key>
    <true/>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/sh</string>
        <string>/usr/local/bin/my-script.sh</string>
    </array>
    <key>StartInterval</key>
    <integer>1800</integer>
</dict>
</plist>

This agent will run the shell script /usr/local/bin/my-script.sh every 1800 seconds (every 30 minutes). You can also have task run on certain dates/times (basically launchd can do everything cron can do) or you can even disable "OnDemand" causing launchd to keep the process permanently running (if it quits or crashes, launchd will immediately restart it). You can even limit how much resources a process may use (as said before, Lingon shows all these settings in a nice UI interface).

Update: Even though OnDemand is still supported, it is deprecated. The new setting is named KeepAlive, which makes much more sense. It can have a boolean value, in which case it is the exact opposite of OnDemand (setting it to false behaves as if OnDemand is true and the other way round). The great new feature is, that it can also have a dictionary value instead of a boolean one. If it has a dictionary value, you have a couple of extra options that give you more fine grain control under which circumstances the task shall be kept alive. E.g. it is only kept alive as long as the program terminated with an exit code of zero, only as long as a certain file/directory on disk exists, only if another task is also alive, or only if the network is currently up.

Also you can manually enable/disable tasks via command line:

launchctl <command> <parameter>

command can be load or unload, to load a plist or unload it again, in which case parameter is the path to the file. Or command can be start or stop, to just start or stop such a task, in which case parameter is the label (com.example.my-fancy-task). Other commands and options exist as well.

See Apple's documentation of the plist format and of the launchctl command line tool (note that you can select the OS X version on top, since the format/options do vary between different OS X releases)

Comments