Scheduling jobs with launchd and friends

September 2, 2019

When I tried to schedule a job for the first time in macOS, I faced the old news that Apple deprecated the good old cron in favor of launchd and a family of utilities to launch and debug jobs.

While I believe cron is going to outlive us, I decided to use launchd because it allows running jobs after the computer wakes up from sleep.

Here are the notes I took during the process:

Defining jobs

launchd makes a distinction between 'Daemons' which are processes run by the root and 'Agents' which are processes run by users.

To describe jobs, you use 'Property List Files' (.plist files from now on.)

.plist files use XML syntax to describe a job, and depending on where they are, fulfill a different purpose:

Location Purpose
~/Library/LaunchAgents Per-user agents provided by the user.
/Library/LaunchAgents Per-user agents provided by the administrator.
/Library/LaunchDaemons System-wide daemons provided by the administrator.
/System/Library/LaunchAgents Per-user agents provided by Apple.
/System/Library/LaunchDaemons System-wide daemons provided by Apple.

After the system boots and the kernel is running, launchd runs to finish the system initialization. As part of that initialization, it goes through the following steps:

  1. Loads the parameters for each launch-on-demand system-level daemon from the property list files found in /System/Library/LaunchDaemons/ and /Library/LaunchDaemons/.
  2. Register the sockets and file descriptors requested by those daemons.
  3. Launches any daemons that requested to be running all the time.
  4. As requests for a particular service arrive, launches the corresponding daemon and passes the request to it.
  5. When the system shuts down, it sends a SIGTERM signal to all the daemons.

Structure of a Property List File

In its most basic form a plist file only requires a few keys defined:

<?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.example.copyfiles</string>

<key>ProgramArguments</key>
<array>
<string>cp</string>
<string>dir1</string>
<string>dir2</string>
</array>

<key>KeepAlive</key>
<true/>
</dict>
</plist>

A description of all valid keys lives in the launchd.plist man page.

Notable keys are StartInterval and StartCalendarInterval, that allows you to define time intervals to run the process.

<key>StartCalendarInterval</key>
<dict>
<key>Minute</key>
<integer>45</integer>
<key>Hour</key>
<integer>13</integer>
<key>Day</key>
<integer>7</integer>
</dict>

Tips

Logging

One of the first things you want to do is enable logging to know what is going on in your process.

To do this you need to set Debug to true and provide paths for standard output and error output:

<key>StandardOutPath</key>
<string>/var/log/myjob.log</string>

<key>StandardErrorPath</key>
<string>/var/log/myjob.log</string>

<key>Debug</key>
<true/>

tip: the files must be in a directory with writing access.

Loading / Reloading

Once a job is in the right folder, you have to either restart your computer to make it load, or instruct launchctl to load the process with:

$ launchctl load ~/Library/LaunchAgents/com.myprocess.plist

If you make changes to your .pid file, you'll want to reload the script in a two-step process:

$ launchctl unload ~/Library/LaunchAgents/com.myprocess.plist
$ launchctl load ~/Library/LaunchAgents/com.myprocess.plist

Debugging

This is a convenient alternative to editing the launchd.plist for the service and then reloading. To use launchctl to trigger a debug process:

$ sudo launchctl debug gui/$UID/com.myprocess.plist --stdout --stderr

note: $UID is your user ID, it's a variable automatically set for you.

Once launchctl is listening, you need to open another terminal tab and start your process as described below. Everything is logged in the listening tab.

note: launchctl debug allows you to do many more than this, check out the man page.

Starting a process

To kick-start a process at any time:

$ launchctl start com.myprocess.plist

Resources