FAM Tasks

This chapter describes MzFAM's high-level interface, which, hopefully, will fulfil all you file monitoring needs (if that's not the case you can either bug me to improve the interface or roll your own using the low-level interface detailed in subsequent chapters).

We begin with some concrete usage examples in section 2.1 to get the gist of how FAM tasks work (as you'll see, there's no rocket science involved), followed by a review of some knobs you can use to customize their behaviour in section 2.2. Finally, section 2.3 tells you all you need to know about what and when FAM events are generated by the system in response to file alterations.

2.1  Basic usage

A FAM task is just a thread that monitors a set of files or directories. Whenever it notices a change in any of them, the task checks whether you've expressed interest on being notified for that kind of change and invokes the corresponding procedure (also provided by you as a token of your interest) with a so-called FAM event as its argument. A FAM event is just a struct containing the monitored path, the path of the altered file (which may be different from the monitored path when the latter is a directory), a symbol (or event type) describing the type of alteration (file modified, created, deleted and so on) and a timestamp.

So, using FAM tasks is a simple matter of:

Let us spell out this process in a fully functional, if a bit silly, program:

(require (planet "fam-task.ss" ("jao" "mzfam.plt" 1 0)))

(define ft (fam-task-create))

(define (display-event event)
  (let ((mp (fam-event-monitored-path event))
        (p (fam-event-path event))
        (type (fam-event-type->string (fam-event-type event)))
        (time (fam-event-timestamp event)))
    (printf "~A: ~A while monitoring ~A (~A)~%" p type mp time)))

(when (fam-task-add-path ft "/home/jao/tmp" display-event)
  (fam-task-join ft))

(error "Ooops, fam task exited unexpectedly!")

The above code shows the four selectors available to extract relevant information from FAM events you receive, besides an otherwise useless function that comes in handy when writing tutorial snippets. You can also see the most simple way of adding a path to the set monitored by a FAM task (using fam-task-add-path). Note that the monitoring thread is not started until we call fam-task-join, which, in addition, makes the current thread wait on the FAM task's. The alternative is to use fam-task-start, which returns after spawning the monitoring thread and let's you go on with your business. Those business may include, incidentally, adding new paths to be monitored or removing previously added ones (as in (fam-task-remove-path ft "/home/jao/tmp")), or even stoping the monitoring task ((fam-task-stop ft)). This slightly more interesting program exercises all those abilities:

(require (planet "fam-task.ss" ("jao" "mzfam.plt" 1 0)))

(define ft (fam-task-create))

(define (display-event event)
  ;; as above ...)

(unless (fam-task-start ft)
  (error "Could not start monitoring task"))

(printf "Monitoring using ~A started~%"
        (if (fam-use-native?) "FAM/Gamin daemon" "scheme inplementation"))

(define (display-event event)
  (printf "* ~A: ~A (~A)~%"
          (fam-event-path event)
          (fam-event-type->string (fam-event-type event))
          (fam-event-monitored-path event)))

(define (read-op)
  (printf "(a)dd, (r)emove, (s)uspend, r(e)sume, (p)rint, (q)uit: ")
  (let ((op (read))) (read-line) op))

(define (read-path) (printf "Path: ") (read-line))

(let loop ((op (read-op)))
  (if (case op
        ((p) (display (fam-task-monitored-paths ft)) (newline) #t)
        ((a) (fam-task-add-path ft (read-path) display-event))
        ((r) (fam-task-remove-path ft (read-path)))
        ((s) (fam-task-suspend-monitoring ft (read-path)))
        ((e) (fam-task-resume-monitoring ft (read-path)))
        (else #f))
      (display "OK")
      (display "KO"))
  (if (eq? op 'q) (fam-task-stop ft) (loop (read-op))))

(when (not (null? (fam-task-monitored-paths ft)))
  (display "You left some paths to monitor...")
  (unless (fam-task-join ft) (error "Couldn't restart monitoring")))

This example brings into play all the procedures exported by fam-task, with the exception of the predicte fam-task-running? (whose meaning I'll leave to the reader's imagination). As you have surely noticed, there's no problem restarting a stopped FAM task, the only caveat being that it remembers the paths it was monitoring but will not catch any event that happened while it was dead. One can also suspend and resume monitoring of a previously added path, the difference with just add-remove-add being that events associated with a suspended path are duly reported upon resuming. The parameter fam-use-native? tells us whether MzFAM has been able to contact a running FAM/Gamin daemon (and the associated C library): if that's the case, (fam-use-native?) evaluates to #t; otherwise the fall-back, pure-scheme implementation is used.

Incidentally, the monitoring program above has the dubious virtue of exposing a situation where you may be tempted to use one of the low-level modules directly: you just need to play with it a little and get nervous when the output of display-event gets mixed with the loop's prompts. But of course, judicious use of MzScheme thread synchronisation primitives should be a better option, or using the simple event stream described in section 2.3.2.

There's also (at least) one more clumsy thing about the above examples: we're repeatedly using the same event handler. No worries: fam-task-create accepts a default event handler as argument, which gets used for monitored paths added without a specific one. That is, you could fix the examples above by replacing the right calls by something along the lines of: (define ft (fam-task-create display-event)) ... (fam-task-add-path ft path) ...

That's basically it. The next section explores some of the tweaks at your disposal to fine-tune the behaviour of FAM tasks.

2.2  Nuts and bolts

2.2.1  FAM task flavours

As mentioned, fam-task-create will do its very best to contact a running FAM or Gamin daemon and only when that attempt fails will fall back to using the pure-Scheme monitor. Nevertheless, you can instruct MzFAM to use the scheme implementation (provided by the fam-mz module) no matter what with (fam-use-native? #f), either in a line of its own or as part of a parameterize form. The Scheme implementation tries hard to provide the same functionality as the native one, but falls short in a few (hopefully minor2) respects:

As hinted above, FAM tasks can operate in two modes, namely, polling and blocking. In polling mode (the default) the thread sleeps for a while, checks for new events, invokes the needed callback procedures, processes requests for new, resumed or suspended paths and goes back to sleep. The sleep period can be provided as an optional argument to fam-task-create. Its default value (given by the parameter fam-task-default-period) is 0.1 (i.e., ten milliseconds). Thus, if you want your FAM task to perfom its duties (approximately) every half second, just create it with (fam-task-create 0.5).

When the FAM/Gamin daemon is available, FAM tasks can operate in blocking mode: they sleep as long as there're no pending events. You request this operation mode using 0 as the argument passed to fam-task-create. In principle, this may sound like a good way to save a few CPU cycles, but a task that awakes every, say, 10 ms is not what you'd call a resource hog and, more importantly, your FAM task won't be able to attend new monitoring requests unless a new event awakes it. Thus, blocking operation is probably only a good idea when the set of monitored paths is fixed once and for all beforehand, or when you'll want to modify it only if/when file alterations occur.

2.2.2  Path monitoring knobs

As we have seen, we tell FAM tasks to monitor a given path using fam-task-add-path, which in its simplest form takes a pathname and a procedure of arity one as its arguments. The pathname can be either absolute or relative: in the latter case, it's resolved with respect the current working directory. The small print of how pathnames are handled includes:

We have already mentioned that the third argument of fam-task-add-path is actually optional. In fact, this procedure takes not one but three optional arguments, namely, an event handler, a list of event types, and a recursion flag (use #f on any of them to make it take its default value).

The list of event types is, well, a list of symbols representing the events you're interested in (see section 2.3 for the complete list of event types). For instance, this invocation requests notifications (using the default event handler) for just creation and modification events associated with the given path:

(fam-task-add-path ft path #f '(fam-event-modified fam-event-created))

The final optional argument is only using when the monitored path is a directory. By default, monitoring a directory results in events affecting its contents, but not the contents of any of its subdirectories. In short, directory monitoring is not recursive by default. Passing #t as the last (fifth) argument of fam-task-add-path modifies this behaviour: any subdirectory found (or subsequently created) inside the original path is added to list of monitored paths, recursively. For instance, calling

(fam-task-add-path ft "/tmp/foo" handler events #t)

means that, if a directory "/tmp/foo/bar/" is detected during monitoring, the system will automatically make the following call for you:

(fam-task-add-path ft "/tmp/foo/bar" handler events #t)

Alternatively, you can provide an integer recursion level as the last parameter (where #f stands for 0 and #t for infinite), which will be decremented each time a new subdirectory level is entered. That is, if you add "/tmp/foo" with

(fam-task-add-path ft "/tmp/foo" handler events n)

"/tmp/foo/bar" (as well as any other direct child of foo) will be added using

(fam-task-add-path ft "/tmp/foo/bar" handler events (- n 1))

and yes, you guessed right: zero means the recursion stops.

2.3  FAM events

2.3.1  Event types

FAM events come with a type (a symbol) attached describing the kind of alteration a given file has suffered (this file's path can differ from the monitored one when the latter is a directory). The next table lists all possible type symbols together with their meaning.

Events reported in all platforms
fam-event-found File detected at monitoring start
fam-event-eol End of file detected list
fam-event-modified File or directory modified
fam-event-created File or directory created inside a monitored folder
fam-event-deleted File or directory deleted
Events reported only by FAM/Gamin
fam-event-moved File or directory moved
fam-event-exec-start Monitored program started execution
fam-event-exec-stop Monitored program ended execution
fam-event-acknowledge Generated right after a cancellation request is satisfied

I think all events are self-explanatory, with maybe the exception of 'found' and 'eol'. When you request that a file be monitored, MzFAM generates a fam-event-found event for that file (if it exists). When the request is that a directory be monitored, MzFAM generates a fam-event-found event for that directory (if it exists) and every file contained in that directory. After generating all 'found' events for the directory, a single fam-event-eol is issued.

2.3.2  Event streams

For simple (or not so simple) cases, your program may be interested in playing the role of a sequential consumer of events produced by a FAM task. That is to say, you may be interested in accessing available events synchronously, avoiding being upset by asynchronous callbacks. With those cases in mind (and just because it's really easy to implement3), fam-task exports a procedure, fam-make-event-stream which creates an event stream. You can pass that stream to fam-task-add-path or fam-task-create as the event handler, and use it yourself (calling it without arguments) to perform non-blocking read-event operations. Finally, calling the object returned by fam-make-event-stream with #t as argument you block until a new event is available. Here's an incomplete code snippet that shows how one uses an event stream:

(define es (fam-make-event-stream))
(define ft (fam-task-create es))

(fam-task-add-path ft "/tmp")

(let loop ((next-event (es #t))) ;; this call is blocking
  (printf "Event: ~A" (fam-event-type next-event))
  (let loop () (if (es) (loop))) ;; consume events without blocking
  (loop (es #t))) ;; wait for next one

Surely, you'll come up with some more imaginative uses of event streams. The useful bit here is that you can go on with your business, and consume pending events only as and when needed.

2 If these issues really matter to you and you're on GNU/Linux or any other system supporting the FAM daemon, the best solution is of course installing the latter and let MzFAM use it. I'm told (thank you, Chris) that on Windows there's a similar C-level interface, and one could use PLT's FFI to hook on that (as the fam module does with libfam) -- I don't use Windows, so unless a kind contributor steps up, don't hold your breath on that. A similar kernel-level subsystem for Mac OS X exists, and work is underway for future versions of MzFAM.

3 Yes, libraries should be designed not by piling feature on top of feature, I know, but this one was just a matter of using an asyncronous channel. Too easy to resist.