Skip to main content

% ins3cure.com

Launchd

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:

  1. Boot the system
  2. 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

Oh, God, look at PID 1

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:

  1. A task to schedule
  2. 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>	
If you are still sleeping at 9:00 AM the job will not run. However 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 if you specify both day and weekday, the job will run if either day or weekday match.

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!

References

About Daemons and Services

comments powered by Disqus