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:
Creating a FAM task instance using
(fam-task-create)
.Registering one or more paths to be monitored (using
fam-task-add-path
), providing, for each one, a procedure handling events and, optionally, a list of event types you're interested on.Starting (or joining) the FAM task using
fam-task-start
(fam-task-join
).
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")) (newline) (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:
Some, let's say, exotic events, such as stop/start execution (see section 2.3 for details) are not detected, because the Scheme implementation operates exclusively on the basis of file modification times.
Blocking monitoring (see below) is only supported in FAM-based monitoring.
Some legit events may be missed during the sleep period of the polling thread -- for instance, if a file is deleted and re-created quickly enough, the Scheme monitor may report a 'file modified' event instead of a deletion/creation pair.
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:
If the pathname points to an existing file, the system figures by itself whether it denotes a directory or a regular file.
Pathnames pointing to non-existent files are allowed: if they spring into existence afterwards, they'll be monitored. A little help from you is needed, though: if you mean to monitor a would-be directory, the pathname should end with a path separator character (e.g., '/' in Unix systems). This limitation does not apply to files created during (possibly recursive) directory monitoring.
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.