Sometimes you just want the dang thing to pass butter. Introducing launchutil.
15 Jun 2023 Charles Choi
There’s an old yet useful feature in macOS where you can get the time announced periodically, typically at the top of the hour.
One thing though: Turning this feature on means manually turning it on and having it repeatedly announce until you manually turn it off. This begs for automation, especially if you want the time announced only during working hours (say 9am to 5pm). Since macOS is my daily driver, this is where we talk about launchd, which since Mac OS X 10.4 has been the system process that manages daemons and agents. There’s plenty of posts describing it and how launchd
works (see references below) so I won’t go over that here. What I will say though is that working with launchd
via the launchctl
command line utility is a PITA.
By and large my pain points in using launchctl
are this:
-
Creating a launch script means you need to write it in XML, following a schema that I find impossible to remember.
-
Installing the launch script means you need to work with two different directories: 1) where you create the launch script and 2) where you need to install it (typically
$HOME/Library/LaunchAgents
). -
Managing the launch script (that is starting, stopping, getting its status) using
launchctl
requires different references to the launch script/service which means different command line arguments to access the same thing. This makes the ergonomics of usinglaunchctl
punishing, especially when debugging the launch script.
Having worked with macOS for well over a decade, I’ve felt the woe of writing a number of launch scripts and encountering all of the pain points above. So I’ve decided to do something about it.
Introducing launchutil, a helper utility to support creating and running a simple macOS launchd
service. It is written in Python and is expressly designed to have the following features:
- Easy creation of a working XML file for daily scheduling that you can edit to taste.
- Uses the launch script name as the reference to the job/service you want to run. (You can still use the service name though.)
- Easy installation and removal of the launch script.
Let’s see launchutil
at work: Imagine that you want to create a job that invokes the say
command to say “hello there” at 14:00 (2pm) and 15:15 (3:15 pm) everyday. The command invocation would look like:
$ launchutil create --program /usr/bin/say --program-arguments hello there --daily 14:00 15:15 --execute com.yummymelon.sayhello
Alternately, you can use short arguments to achieve the same result.
$ launchutil create -p /usr/bin/say -a hello there -d 14:00 15:15 -x com.yummymelon.sayhello
Running the above generates the launch script named com.yummymelon.sayhello.plist
whose contents are shown below:
<?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.yummymelon.sayhello</string>
<key>Program</key>
<string>/usr/bin/say</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/say</string>
<string>hello</string>
<string>there</string>
</array>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>14</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>15</integer>
<key>Minute</key>
<integer>15</integer>
</dict>
</array>
</dict>
</plist>
Note that the launch script file name is built off the service name. launchutil
relies on this convention to support the command-line ergonomics to infer the service name from the launch script file name and vice-versa.
Installing the launch script into the default directory $HOME/Library/LaunchAgents
is achieved with the following command:
$ launchutil install com.yummymelon.sayhello.plist -x
To start the service:
$ launchutil start com.yummymelon.sayhello.plist -x
To get the status of the service:
$ launchutil status com.yummymelon.sayhello.plist -x
To stop the service:
$ launchutil stop com.yummymelon.sayhello.plist -x
To uninstall the service:
$ launchutil stop com.yummymelon.sayhello.plist -x
Note that in all the above commands:
- No navigation or references to the installed directory.
- Only reference the launch script file name (or service name).
Say Time at the Top of the Hour
So as promised at the start of this post, a launchutil
example that will announce the time daily from 9am to 5pm can be found at https://github.com/kickingvegas/launchutil/tree/main/examples
Getting launchutil
If you’ve made all the way here and are still interested, you can get launchutil
at https://github.com/kickingvegas/launchutil.
For the sake of simplicity, there is no pip · PyPI packaging; just install the single file Python 3 script into your bin
directory. It has no dependencies to any packages outside of the basic macOS install.
If you have any feedback, please let me know at the GitHub discussion board for launchutil
.