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"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.disney.fa.runradmind</string>
<key>ProgramArguments</key>
<array>
<string>/Users/Shared/radmindNow</string>
<string>-force</string>
</array>
<key>WatchPaths</key>
<array>
<string>/private/var/radmind/client/.radmindOnDemand</string>
</array>
<key>OnDemand</key>
<true/>
</dict>
</plist>

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: http://developer.apple.com/technotes/tn2004/tn2124.html 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

6 thoughts on “launchd gotcha

  1. Jaharmi says:

    I literally found this out independently the same week you posted about this. And I was attempting to use a LaunchDaemon for the same reason — logout-driven background Radminding! This is getting really weird, like we’re always on the same wavelength or something. 🙂

  2. Actually, I’m using a logout hook for logout-driven radminding on Panther and Tiger. I’m using a LaunchDaemon to trigger a radmind run while the user is still logged in. This is needed in our environment when a user is connected via VPN or 802.1x – logging out kills their connection, so logging out and then radminding won’t work.

  3. Jaharmi says:

    Well, it was still a LaunchDaemon-driven Radmind session, tied to a WatchPath. I’m already using launchd to some extent, because I just run my normal overnight Radmind sessions via the periodic daily routine. (This works unless the computers are asleep through 10.4.6, and then the plot thickens — you’re never really sure when the missed job will be rescheduled.) But I’m also trying to get a Radmind session that won’t delay logout, but is tied to logout, running as that is a dependable during-the-day trigger in my environment.

  4. Conrad says:

    Tough man – can’t imagine locking down someone’s screen saver preference – is one of the few ways to express yourself on the computer – still i’m sure there are some corporates that require it. Shame. But good solution regardless.

  5. Tony says:

    I have this plist file that watches the “WatchDirectory” in my home directory for any changes to files or added files, scripts runs, or files deleted. If a change occurs, it calls the /usr/bin/logger command which basically writes to the /var/log/system.log file. All it writes though is very vague every time, nothing specific: for example

    “Jul 21 19:34:41 TonyMAC Tony[16512]: path modified”

    Do you know how I can have it write more specific messages like, “a file was deleted”, a script was ran or even a simple – “the change happened in this directory” to the /var/log/system.log file Thanks in advance.

    Label
    logger
    ProgramArguments

    /usr/bin/logger
    path modified

    WatchPaths

    /Users/Tony/WatchDirectory

Comments are closed.