launchd gotcha

While working on a way for a user to run radmind on demand without logging out, I started playing with launchd jobs.

radmind needs to run as root to do its thing, and I need an unprivileged user to be able to tell radmind to run. For most users, I accomplish that with a logout hook. The user runs an app that touches a file and then tells the system to logout; a logout hook (which runs as root) sees the file exists and runs radmind. This is good for several reasons: radmind gets to run as root, and the user is logged out, so radmind can make changes to the system without stomping on the user.

But what happens with laptop users who are connected via VPN or 802.1x? If they logout, the VPN or 802.1x connection is broken and they can no longer connect to the radmind server. So I need to be able to let the user run radmind even while they are logged in. The potential exists for radmind to stomp on them, but without this option, there may be users who never run radmind because they are never on the (hard-wired) network.

So I thought of using a launchd job to run radmind with an adaptation of the method used to run it at logout. I'd set up the launchd job in /Library/LaunchDaemons, so it would run as root, and using the "WatchPaths" key, have it watch for my trigger file. Sounds good, right?

Here's the plist for the job:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
<plist version="1.0">

But I couldn't get it to work consistantly. I'd start the launchd job, touch the file at the WatchPath (/private/var/radmind/client/.radmindOnDemand) and nothing would happen. I'd unload the job, reload it, touch the file again, and then it would fire. Touch it again, and it wouldn't. Maddening.So I started looking around for info on debugging launchd and found this: I turned on debugging, and eventually figured it out when I saw lines like this in my /var/log/launchd.log:

May 10 16:53:00 rhapsody launchd: com.disney.fa.runradmind: watch path invalidated: /private/var/radmind/client/.radmindOnDemand

May 10 16:54:09 rhapsody launchd: com.disney.fa.runradmind: open("/private/var/radmind/client/.radmindOnDemand", O_EVTONLY): No such file or directory

It turns out that if a file is listed as a launchd job WatchPath, it must exist at all times, or launchd removes the path from its list of paths to watch. So you can't watch for the creation of a certain file; you can only watch for changes to the file. Alternately, you could watch a directory – you could then trigger the action by creating, deleting or changing a file inside the directory.

This explains the behavior I was seeing. If the file /private/var/radmind/client/.radmindOnDemand didn't exist when the launchd job was started, it never added it to its list of paths to watch. If it did exist, it watched that path, and when I touched it, it launched my "radmindNow" script. But that "radmindNow" script removed the /private/var/radmind/client/.radmindOnDemand file (similar to removing the /private/var/radmind/client/.radmindAtLogout file so it wouldn't run radmind at EVERY logout) – once the /private/var/radmind/client/.radmindOnDemand file was removed, launchd stopped watching that path. So I could trigger radmind once, but never again without unloading/reloading the launchd job.

So to generalize what I learned:

The WatchPaths key in a launchd job .plist file must refer to files or directories that exist at all times the launchd job is active. You cannot use the WatchPaths key to watch for the creation of a specific file. If at any time the launchd job is active a path in WatchPaths is removed, it is also removed from launchd's internal list of paths to watch. If you subsequently recreate the path, it will not be re-added to launchd's list of paths to watch.

Perhaps this will save you headaches in the future! 

launchd gotcha