10

I have some services like this that I'd like to run almost immediately after files are modified.

<?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>test</string>
    <key>ProgramArguments</key>
    <array>     
        <string>say</string>
        <string>a</string>
    </array>
    <key>WatchPaths</key>
    <array>
        <string>/Users/username/Desktop/</string>
    </array>
</dict>
</plist>

Even if ThrottleInterval was set to 1 or 0, they are only run at most every 10 seconds.

9/9/12 4:57:05.457 PM com.apple.launchd.peruser.501[138]: (test) Throttling respawn: Will start in 7 seconds
9/9/12 4:57:09.541 PM com.apple.launchd.peruser.501[138]: (test) Throttling respawn: Will start in 3 seconds

man launchd.plist only says that programs aren't run more than every 10 seconds by default, but doesn't mention that ThrottleInterval couldn't be set below that.

ThrottleInterval <integer>
This key lets one override the default throttling policy imposed on jobs by launchd.
The value is in seconds, and by default, jobs will not be spawned more than once
every 10 seconds.  The principle behind this is that jobs should linger around just
in case they are needed again in the near future. This not only reduces the latency
of responses, but it encourages developers to amortize the cost of program invoca-
tion.

You could keep the program or script running for 10 seconds and watch for changes every second:

#!/bin/bash

start=$(date +%s)
prev=

until (( $(date +%s) >= $start + 10 )); do
    new=$(stat -f %m ~/Desktop/)
    [[ $prev != $new ]] && say a
    prev=$new
    sleep 1
done

Or the same in Ruby:

#!/usr/bin/env ruby

start = Time.now
prev = nil

until Time.now >= start + 10
  current = File.mtime("#{ENV['HOME']}/Desktop/")
  `say a` if current != prev
  prev = current
  sleep 1
end

But is there some way to bypass or decrease the time limit? It also applies to folder actions.

3 Answers 3

10

There is no way to bypass or decrease the time limit.

Apple's documentation regarding Creating Launchd Jobs states the following:

Important If your daemon shuts down too quickly after being launched, launchd may think it has crashed. Daemons that continue this behavior may be suspended and not launched again when future requests arrive. To avoid this behavior, do not shut down for at least 10 seconds after launch.

Your program or script needs to keep running for at least 10 seconds. Consider implementing a loop to check for file modification dates in the last ten seconds, sleeping for ten seconds, and repeating.

Alternatively, you can watch specific files using the kqueue or FSEvents APIs. This StackOverflow question may be helpful, File-level filesystem change notification in Mac OS X.

2

You could keep your script running in a loop checking for modified files instead of quitting when it is done. Have it sleep for a few seconds after checking for the modified files. If it finds modified files, continue with the script. If not, sleep again.

Then have launchd start your script every x minutes just in case the previous run dies. Code the beginning of your script to check if another instance is already running and if so, quit itself.

2
  • launchd doesn't seem to start another instance if the previous one is still running.
    – Lri
    Commented Sep 11, 2012 at 15:26
  • launchd will not launch multiple instances of the same job ticket. Commented Sep 11, 2012 at 17:13
1

If you need start a script more often than every 10 seconds, it can be expensive in term of "forking" (read: allocating memory, starting new processes etc).

Therefore, in this case is the best to write your own "daemon" (program, what runs in the background)

I recommend you use an "more capable" language as BASH (my favorite is "perl", but ruby is OK too) because a good daemon handles timeouts, alarms and so on - things what is too hard implement in pure bash. (Of course, the daemon can run your bash scripts too - if needed). The basics are:

  • script what is running endless and waiting for some event. The event can be some network input, or simple timer or anything like. When the event arrives (e.g. the wait state ended) the script will do what you want and the cycle repeats.

In the perl world already exists modules what tuns your script as "daemon" process, for example Proc::Daemon. I'm not experienced with ruby, but this article can help you.

You can start your daemon process via Launchd at the system startup, or from automator app when you logging in, or from the Terminal manually.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .