Scheduling jobs with launchd and friends
September 2, 2019When 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:
- Loads the parameters for each launch-on-demand system-level daemon from the property list files found in
/System/Library/LaunchDaemons/
and/Library/LaunchDaemons/
. - Register the sockets and file descriptors requested by those daemons.
- Launches any daemons that requested to be running all the time.
- As requests for a particular service arrive, launches the corresponding daemon and passes the request to it.
- 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:
Label
: a unique identifier.ProgramArguments
: arguments to launch the daemon.KeepAlive
: specifies whether the daemon launches on-demand or must always be running.
<?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