A new update to Macaroni is due soon, and one of the changes is that I'm finally updating it to use launchd to start it up, instead of the older StartupItems approach. It's been possible to use launchd since 10.4 was released back in 2005. But Macaroni's older than that, so I couldn't make the switch right away without splitting it into two versions.

Launchd is very different from the old approach but one thing that was regrettably not included is the option to load and unload jobs from code. That's fine if you want to install a launchd job and have it run always, no matter what. If you want to be able to load and unload jobs when a user requests it, it's not so great. The only option Apple supplies is the launchctl command-line tool. At the command line you end up doing something like

launchctl load -w com.example.foo.plist

You can wire that up to NSTask to do it from an application, but that's always an ugly solution.

The existing launch.h API

It'd be nice if there was a launchd API, and in fact there is-- developers, check out /usr/include/launch.h. Read it carefully, because the comments there are all the documentation there is. I'll wait. Back so soon? There's also demo code showing the launchd "check-in" procedure, but that's it.

On its own that's not a lot of help, but it provides useful clues. Specifically, it's apparent that the launch.h API defines a messaging protocol that user-space apps can use to communicate with launchd. The sample code provides some clues as to how that works. And since launchctl isn't setuid-root or anything fancy, using this protocol doesn't require any special permissions for apps.

A higher level approach

I could puzzle out the rest of the protocol piece by piece, but there's a better way, a much more complete demonstration. All of launchd-- including launchctl-- is open source and readily available. Since I basically want to do what launchctl does, only from my own code, I decided the best way would be to take the launchctl source code and convert it from a command-line tool into a simple library.

This is actually pretty straightforward. Each of launchctl's "verbs" are implemented by functions in the aptly-named launchctl.c. Comment out main(), add some nice entry points, and yeah, it's almost that easy. I don't need all of launchctl's options, so I implemented the ones I needed plus a few extras. The rest are left as an exercise for the reader.

int launchctl_load_path(CFStringRef plistPath, bool writeFlag,
    bool forceFlag);
int launchctl_unload_path(CFStringRef plistPath, bool writeFlag);

int launchctl_start(CFStringRef jobLabel);
int launchctl_stop(CFStringRef jobLabel);

CFArrayRef launchctl_list();

int launchctl_setenv(CFStringRef key, CFStringRef value);
int launchctl_unsetenv(CFStringRef key);
CFDictionaryRef launchctl_export();
CFStringRef launchctl_getenv(CFStringRef key);

These correspond to existing launchctl commands in a manner that'll be obvious to anyone who's read the launchctl man page.

I found that launchctl.c had a surprising structure, in that the command-line arguments are not completely processed by main(). Instead, main() reads enough to decide which verb is desired and then lets other functions continue processing argv. I wanted to keep the modified code as close to the original as possible, so wherever it made sense my code constructs a fake argv and just drops that into the existing code.

In other cases that didn't make sense. For example the existing "launchctl list" code prints loaded jobs to standard output. But if you're calling launchctl_list() you'd probably prefer to get that list in your code. The semantics of communicating with launchd (as well as existing launch.h functions like launch_data_dict_iterate()) made it impossible to gather up that information without extreme steps like redirecting standard output (which might interfere with other things going on in the application). In those cases I ended up reimplementing the existing functions rather than using the fake-argv approach.

Of course the usual permissions related to launchctl apply here as well. If an existing launchd job is owned by root, you can't unload it unless you're root. Conversely, following launchd's approach, if a launchd job is owned by a user, you can't unload it unless you are that user. Being root doesn't help there, you'd presumably have to setuid() to the appropriate user first.

Anyway, the code is available, and I'd be interested in feedback about it from other developers.

This is based on launchd 106.20, the version from Mac OS X 10.4.9 and the most recent version available from Apple as of right now.

To use it in a project, just add all of the files and include launchctl.h in any source files that need to use the API. Since the modifications use CF types you'll also need to be linking at least CoreFoundation, if not something like Foundation or Cocoa.