Launchd 101
- EN
- ES
Table of Contents
After writing this post the logical next step is writing something about launchd.
launchd is a tool created by Apple some years ago (launched in 2005 with OS X 10.4 Tiger). It is open source and licensed under the Apache License.
launchd has two main goals:
- Boot the system
- Maintain services
If you think that it resembles systemd no, you are not crazy: In Rethinking PID 1 Lennart Poettering (a Red Hat engineer author of systemd) mentions launchd as a source of inspiration.
I learnt about launchd shortly after getting my first Mac, when I tried to find the cron daemon.
##
The cron daemon
The cron daemon still exists in macOS Catalina but if you read the documentation (man cron
) you will find:
(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.)
Ok, so no more cron jobs, from now on I will use launchd instead.
##
launchd does a lot of stuff
launchd is PID 1 so it basically is almighty God. It replaces at least good old init, rc scripts, SystemStarter, inetd, crond and watchdogd.
However this humble post is not about all that wonderful stuff but only about scheduling tasks.
##
launchd as cron replacement
So how can we use launchd to schedule tasks? We’ll need two components:
- A task to schedule
- A property list (plist) file
##
A word about plist files
According to wikipedia, property list files are files that store serialized objects. They are often used to store user’s settings.
Apple Developer documentation is a bit more specific:
An information property list file is a structured text file that contains essential configuration information for a bundled executable. The file itself is typically encoded using the Unicode UTF-8 encoding and the contents are structured using XML. The root XML node is a dictionary, whose contents are a set of keys and values describing different aspects of the bundle. The system uses these keys and values to obtain information about your app and how it is configured. As a result, all bundled executables (plug-ins, frameworks, and apps) are expected to have an information property list file.
In short, they are horrible XML files the store settings and configurations.
##
My first plist file
First thing you see when you read man launchd
(well, not first because it is at the bottom of the man page, but you know what I mean) is where in the system you can have launchd plist files:
FILES
~/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.
First of all we will forget about /System
folder, ok?
With macOS Catalina, ~/Library/LaunchAgents
seems to have disappeared as well so we have /Library
left. We will be initially interested in /Library/LaunchAgents
because we do not want to run system-wide deamons so far.
But keep in mind we have agents and deamons:
- Agents run in user context and can run GUI applications
- Deamons run system wide and do not allow to run GUI applications
but configuration is the same for both agents and deamons.
Now we know where we want our plist file, let’s write our first plist file.
As explained above, plist files are XML files that look like this:
<?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>SomeString</string>
<key>Program</key>
<true/>
<key>Program</key>
<string>/usr/local/bin/ins3cure.sh</string>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
This is probably the simplest plist file we can write.
###
Label
This is the name that will identify the job. I’m not sure whether it is allowed to use the same label multiple times but it does not look a good idea.
###
Program
This is the program you want to run (/usr/local/bin/ins3cure.sh
in the example above).
You can also provide arguments as an array:
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/ins3cure.sh</string>
<string>--ip</string>
<string>1.2.3.4</string>
</array>
If you use ProgramArguments key then you do not need to use Program. But bear in mind that the first argument is the program name. If you have several options or arguments, add a new string line every time you have a blank space.
##
When will my job run?
You can tell the job to run every time the agent is loaded (for instance, when the system boots):
<key>RunAtLoad</key>
<true/>
If you think the cron way of specifying dates is a pain in the ass wait to see this. Let’s see an easy example; this will start the job at 9:00 AM:
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
launchd
will notice that it has a pending job and will run it as soon as the system is available
###
Which are the available keys?
<key>Month</key>
<key>Day</key>
<key>Weekday</key>
<key>Hour</key>
<key>Minute</key>
Keys work the same as in cron, that is:
- Month is 1-12
- Day is 1-31
- Weekday is 0-7 (0 and 7 are Sunday)
- Hour is 0.23
- Minute is 0-59
You can set more that one date. This will run at 9:00 and later at 21:00:
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>21</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
Note the syntax in not only very xml-uncomfortable but also quite limited. Sometimes it may be easier to trigger a job much more often than needed and do a quick check in the program to figure out if it really has to do run or not.
##
Some more options
You can run jobs at specific intervals. For instance:
This will run every 900 seconds:
<key>StartrInterval</key>
<integer>900</integer>
Watch a directory for changes:
<key>WatchPaths</key>
<array>
<string>/path/to/watch</string>
</array>
Set stdout and stderr destination:
<key>StandardOutPath</key>
<string>/path/to/stdout.log</string>
<key>StandardErrorPath</key>
<string>/path/to/stderr.log</string>
Set working directory:
<key>WorkingDirectory</key>
<string>/Users/me/work</string>
See the complete reference in the documentation or man launchd.plist
##
My first scheduled task
Ok, I have my awesome script and a shiny plist file. Now what? You have to load it. You used to have the load command for that but it has been deprecated because it was too easy, so now we have:
% launchctl bootstrap gui/501 /Library/LaunchAgents/brew-check-update.plist
gui/501
is usually the UID of the logged in iser as reporte by id-u
. But it may not, so another way ton find out is:
% logged_in_user=$(ls -l /dev/console | awk '{print $3}')
% uid=$(id -u $logged_in_user)
% echo $uid
501
To unload:
% launchctl bootout gui/501 /Library/LaunchAgents/brew-check-update.plist
To run. Note this time we are using the service name (that should be the same as the plist file name) instead of the path to the file name:
% launchctl kickstart gui/501/brew-check-update
To list:
% launchctl list
PID Status Label
37626 0 com.apple.SafariHistoryServiceAgent
3249 0 com.apple.progressd
[...]
- 0 brew.updates.notification
[...]
If we know the name we can get more information:
% launchctl list brew.updates.notification
{
"LimitLoadToSessionType" = "Aqua";
"Label" = "brew.updates.notification";
"OnDemand" = true;
"LastExitStatus" = 0;
"Program" = "/usr/local/bin/brew-check-update.sh";
};
Enjoy your scheduled jobs!