Writing and Running Launchd .plist files

Ok, this, at first glance looks pretty arcane. But really, it’s not so bad. I wanted to check twice a day for the certificates for my website and email server from the company letsencrypt.com. These free certificates are recognized worldwide but have to be renewed every three months. This can be done automatically, and the binary that does the checking is called ‘certbot’ and is installable from among other options, Homebrew.
So, I wanted to run “certbot renew” twice a day. A set and forget action. If the certs don’t need renewing, the process exits. Nice.

I’ve learned quite a bit about launchd, launchctl and plist files in the process. I’ll list some of the gotchas later.
The best piece of software I’ve come across for creating plist files is “LaunchControl”. I know, almost the same as the process. You can find it here. A truly brillian timesaver.

<?xml version="1.0" encoding="UTF-8"?><span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span>
<!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.letsencrypt.renew</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/certbot</string>
	<string>renew</string> 
    </array>
    <key>StartCalendarInterval</key>
   <array>
    <dict>
        <key>Hour</key>
        <integer>0</integer>
    </dict>
    <dict>
        <key>Hour</key>
        <integer>12</integer>
    </dict>
   </array>
    <key>StandardOutPath</key>
    <string>/Users/robert/certbotlogs/launchd.stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/robert/certbotlogs/launchd.stderr.log</string>
  <key>AbandonProcessGroup</key>
  <true/>
</dict>
</plist>

Now, here’s a simple working example of a plist file you can put in your home User Library. that is, ~/Library/LaunchAgents if you want it to run all the time.
That way its ownership can be you, not root. Remember to unload it when you are finished playing, or it will run all the time. In fact, remove it alltogether when finished so it doesn’t start on your next reboot…

If you put your plist into /Library/LaunchDaemons then the plist file has to be owned by root.
Make sure you put the full path to the binary that you want to run. Launchctl etc doesn’t know about environment path names UNLESS you explicitly tell it. Easy just to set the full binary path and avoid confusion.

This next little example will write the date and time every ten seconds into the log files that I set up.

<?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>AbandonProcessGroup</key>
	<true/>
	<key>Label</key>
	<string>test</string>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/date</string>
		<string>-v+1m</string>
		<string>-v+1y</string>
	</array>
	<key>StandardErrorPath</key>
	<string>/Users/robert/certbotlogs/launchd.stderr.log</string>
	<key>StandardOutPath</key>
	<string>/Users/robert/certbotlogs/launchd.stdout.log</string>
	<key>StartInterval</key>
	<integer>10</integer>
</dict>
</plist>

Now here is something to be careful of. There are two keys for Programs. One called
ProgramArguments
and one called
Program
and you might think they do the same thing. But they don’t.
The first one ProgramArguments is by far the most common and is used to start programs that take options. This works because the program name assumes position argv(0) and the options argv(1) argv(2) and so on.

The key that is just Program takes only the binary that will run. Or the shell script whatever – but no arguments. It occupies the argv(0) position.

If you use it like this,

	<key>Program</key>
	<array>
		<string>/bin/date</string>
		<string>-v+1m</string>
		<string>-v+1y</string>
	</array>

The first string of arguments -v+1m will be ignored. No idea why it was designed like this but it was.

You can use it 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>com.example.app</string>
		<key>Program</key>
		<string>/Users/Me/Scripts/cleanup.sh</string>
		<key>RunAtLoad</key>
		<true/>
	</dict>
</plist>

If your log file is not writable by the user running the plist file – the plist file also won’t run. launchctl will fail to load it.

Last item in this post. If you load LaunchControl the app, be very careful with it. You can REALLY trash your system with it. However, if like me you have been installing and uninstalling a lot of stray items, LaunchControl will greatly assist in cleaning up, or cleaning out old and non-working plist files. Most excellent tool.

If you are making changes to your /Library/LaunchDaemons directory files – you will have to disable SIP. You can look that up on Google. It’s easy. It’s a level of protection you can leave in place for ordinary users, but if you hack about with your system like I do, it can be a real nuisance. However, I normally leave it in place, and only disable it when needed, then re-enable it when done.

My next addition to this will be a script that uses some of the environment variables and the stdin, out and err switches.

Loading vs Starting

Loading a job definition does not necessarily mean to start the job. When a job is started is determined by the job definition. In fact, only when RunAtLoad or KeepAlive have been specified,launchd will start the job unconditionally when it has been loaded.

Last Updated on January 7, 2018 by @R_A_Chalmers