notes from /dev/null

by Charles Choi 최민수


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 using launchctl 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.

References

python   macos   automation

 

AboutMastodonInstagramGitHub

Feeds & TagsGet Captee for macOS

Powered by Pelican