Chapter 8. Monitoring Changes to Files and Directories

The File Alteration Monitor (FAM) monitors changes to files and directories in the filesystem and notifies interested applications of these changes. Your application can use FAM to get an up-to-date view of the filesystem rather than having to poll the filesystem. This chapter describes the required libraries and provides a basic list of steps for using FAM. For more detailed information, refer to the fam(1M) and FAM(3X) reference pages.

This chapter contains these sections:

FAM Overview

Typically, if applications need to monitor the status of a file or directory, they must periodically poll the filesystem. FAM provides a more efficient and convenient method.

FAM consists of the FAM daemon, fam, and a library for interacting with this daemon. An application can request fam to monitor any files or directories in the filesystem. When fam detects changes to these files, it notifies the application.

This section provides an overview of FAM and describes:

Theory of Operation

FAM uses imon, a pseudo device, to monitor filesystem activity on your system on a file-by-file basis. You can refer to the imon(7) reference page for more information on its operation, but you should not attempt to access imon directly.

When you provide FAM with the name of a file or directory to monitor, FAM passes the request to imon, which begins monitoring the inode corresponding to the pathname. When imon detects a change to an inode that it is monitoring, it notifies FAM, which matches the inode to a corresponding filename. FAM then generates a FAM event on a socket. Your application can either monitor the socket or periodically poll FAM to detect FAM events.

This difference between FAM and imon can produce some unexpected results. For example, if a user moves a file, FAM reports that the file is deleted. The reason is that FAM monitors files by name and not inode, so it doesn't know that the file still exists.


Note: Unlike local files and directories, FAM monitors NFS-mounted files and directories by name rather than by inode.

As another example, consider the case where FAM is monitoring a file. If the user deletes the file, FAM correctly reports that fact. Then FAM polls the directory every few seconds to see if the file has been created. If you need to detect the creation of a given file by name, you may want to monitor the directory in which it will be created and watch for FAM events notifying the creation of a file by that name in the directory.

Whenever FAM is asked to monitor a file/directory that resides on a remote (NFS) filesystem, FAM tries to make a connection to the FAM on the NFS server. If it succeeds, it asks the server fam to monitor the file. The server FAM sends FAM events, and the original FAM translates those events to a form its client can use. If FAM can't connect to FAM on the server, it monitors the file itself by polling every few seconds. Polling over NFS has a high overhead.

FAM Libraries and Include Files

The FAM interface routines are in the libfam library. libfam depends on the libC library. Be sure to specify -lfam before -lC in the compilation or linking command. If you are using fam from a C++ program, libC is included automatically. You must include libC if you are using fam from a C program.

You must include <fam.h> in any source file that uses FAM. You must also include <sys/select.h> if you use the select(2) system call.

The FAM Interface

This section describes the functions you use to access FAM from your application:

Opening and Closing a FAM Connection

The function FAMOpen() opens a connection to fam:

int FAMOpen(FAMConnection* fc)

FAMOpen() returns 0 if successful and -1 if unsuccessful. FAMOpen() initializes the FAMConnection structure passed to it, which you must use in all subsequent FAM procedure calls in your application.

An element of the FAMConnection structure is the file descriptor associated with the socket that FAM uses to communicate with your application. You need this file descriptor to perform select() operations on the socket. You can obtain the file descriptor using the FAMCONNECTION_GETFD() macro:

FAMCONNECTION_GETFD(fc)

The function FAMOpen2 tells FAM the application's name:

int FAMOpen2(FAMConnection* fc, const char* appName)

FAM uses appName when it prints debugging messages.

The function FAMClose() closes a connection to fam:

int FAMClose(FAMConnection* fc)

FAMClose() returns 0 if successful and -1 if unsuccessful.

Monitoring a File or Directory

FAMMonitorDirectory() and FAMMonitorFile() tell FAM to start monitoring a directory or file respectively:

int FAMMonitorDirectory(FAMConnection *fc,
                        char *filename,
                        FAMRequest* fr,
                        void* userData)

int FAMMonitorFile(FAMConnection *fc,
                   char *filename,
                   FAMRequest* fr,
                   void* userData)

FAMMonitorDirectory() monitors not only changes that happens to the contents of the specified directory file, but also to the files in the directory. If the directory contains subdirectories, FAMMonitorDirectory() monitors changes to the subdirectory files, but not the contents of those subdirectories. FAMMonitorFile() monitors only what happens to the specified file. Both functions return 0 if successful and -1 otherwise.

The first argument to these functions is the FAMConnection structure initialized by FAMOpen(). The second argument is the full pathname of the directory or file to monitor. Note that you can't use relative pathnames.

The third argument is a FAMRequest structure that these functions initialize. You can pass this structure to FAMSuspendMonitor(), FAMResumeMonitor(), or FAMCancelMonitor() to respectively suspend, resume, or cancel the monitoring of the file or directory. “Suspending, Resuming, and Canceling Monitoring” further describes these functions.

The fourth argument is a pointer to any arbitrary user data that you want included in the FAMEvent structure returned by FAMNextEvent() when this file or directory changes.

FAM then generates FAM events whenever it detects changes in monitored files or directories. “Detecting Changes to Files and Directories” describes how to detect and interpret these events.

Two similar routines are FAMMonitorDirectory2() and FAMMonitorFile2():

int FAMMonitorDirectory2(FAMConnection *fc,
                        char *filename,
                        FAMRequest* fr);

int FAMMonitorFile2(FAMConnection *fc,
                   char *filename,
                   FAMRequest* fr);

In these routines, the caller picks the request number, not libfam. The caller specifies the request number by putting it in the FAMRequest before calling the routine. For example:

FAMConnection fc;
FAMRequest frq;
...
frq.reqnum = some_number_associated_with_tmp;
if (FAMMonitorDirectory2(&fc, “/tmp”, &frq) < 0)
    perror(“can't monitor /tmp”);

If you use the -2 routines, you must choose unique request numbers. See FAMAcknowledge below.

It's up to you to determine which routines to use: the -2 routines or the original routines.

Suspending, Resuming, and Canceling Monitoring

Once you've begun monitoring a file or directory, you can cancel monitoring or temporarily suspend and later resume monitoring.

FAMSuspendMonitor() temporarily suspends monitoring a file or directory. FAMResumeMonitor() resumes monitoring the file or directory. Suspending file monitoring can be useful when your application does not need to display information about a file (for example, when your application is iconified).


Note: FAM queues any changes that occur to the file or directory while monitoring is suspended. When your application resumes monitoring, FAM notifies it of any changes.

The syntax for these functions is:

int FAMSuspendMonitor(FAMConnection *fc, FAMRequest *fr);

int FAMResumeMonitor(FAMConnection *fc, FAMRequest *fr);

fc is the FAMConnection returned by FAMOpen(), and fr is the FAMRequest returned by either FAMMonitorFile() or FAMMonitorDirectory(). Both functions return 0 if successful and -1 otherwise.

When your application is finished monitoring a file or directory, it should call FAMCancelMonitor():

int FAMCancelMonitor(FAMConnection *fc, FAMRequest *fr)

FAMCancelMonitor() instructs FAM to no longer monitor the file or directory specified by fr. It returns 0 if successful and -1 otherwise.

After you call FAMCancelMonitor(), FAM sends a FAMAcknowledge event. When you've seen the FAMAcknowledge event, you know it's safe to re-use the request number (if you're using the -2 form monitoring routines).

Detecting Changes to Files and Directories

Whenever FAM detects changes in files or directories that it is monitoring, it generates a FAM event. Your application can receive FAM events in one of two ways:

The Select approach
 

Your application performs a select(2) on the file descriptor in the FAMConnection structure returned by FAMOpen(). When this file descriptor becomes active, the application calls FAMNextEvent() to retrieve the pending FAM event.

The Polling approach
 

Your application periodically calls FAMPending() (typically when the system is waiting for input). When FAMPending() returns with a positive return value, your application calls FAMNextEvent() to retrieve the pending FAM events.

FAMPending() has the following syntax:

int FAMPending(FAMConnection *fc)

It returns 1 if there is a FAM event queued, 0 if there is no queued event, and -1 if there is an error. FAMPending() returns immediately (that is, it does not wait for an event).

Once you have determined that there is a FAM event queued, whether by using the select or polling approach, call FAMNextEvent() to retrieve it:

int FAMNextEvent(FAMConnection *fc, FAMEvent *fe)

FAMNextEvent() returns 0 if successful and -1 if there is an error. The first argument to FAMNextEvent() is the FAMConnection structure initialized by FAMOpen(). The second argument is a pointer to a FAMEvent structure, which FAMNextEvent() fills in with information about the FAM event. The format of the FAMEvent structure is:

typedef struct {
    FAMConnection* fc;
    FAMRequest fr;
    char *hostname;
    char filename[PATH_MAX];
    void *userdata;
    FAMCodes code;
    } FAMEvent;

fc is the FAMConnection structure initialized by FAMOpen().

fr is the FAMRequest structure returned by either FAMMonitorFile() or FAMMonitorDirectory() when you requested that FAM monitor the file or directory that changed.

hostname is an obsolete field. Don't use it in your applications.

filename is either the full pathname of the file or directory that you monitored or the name of a file in a directory that you monitored.

userdata is the arbitrary data pointer that you provided when you called either FAMMonitorFile() or FAMMonitorDirectory() to monitor this file or directory. If you used the -2 routine, FAMMonitorDirectory2() or FAMMonitorFile2(), userdata is undefined.

code is an enumerated value of type FAMCodes that describes the change that occurred. It can take any of the following values:

FAMChanged 

Some value of the file or directory that can be obtained with lstat(2) changed.

FAMDeleted 

A file or directory being monitored was deleted.

FAMStartExecuting 

A monitored, executable file started executing. The event occurs only the first time the file is executed.

FAMStopExecuting 

A monitored, executable file that was running finished. If multiple processes from an executable are running, this event is generated only when the last one finishes.

FAMCreated 

A file was created in a directory being monitored.

FAMAcknowledge 

FAM generates a FAMAcknowledge event in response to a call to FAMCancelMonitor(). If you specify an invalid request, that is, a relative path, FAM automatically cancels the request and immediately sends a FAMAcknowledge event.

FAMExists 

When the application requests that a file be monitored, FAM generates a FAMExists event for that file (if it exists). When the application requests that a directory be monitored, FAM generates a FAMExists event for that directory (if it exists) and every file contained in that directory.

FAMEndExist 

When the application requests a file or directory be monitored, FAM generates a FAMEndExist event after the last FAMExists event. (Therefore if you monitor a file, FAM generates a single FAMExists event followed by a FAMEndExist event.)


Note: Prior to IRIX 6.2, FAMNextEvent() did not initialize the filename field in a FAMEndExist event. You should use the request number to find the file or directory these events reference.


Symbolic Links

If you specify the pathname of a symbolic link to FAMMonitorDirectory() or FAMMonitorFile(), FAM monitors only the symbolic link itself, not the target of the link. Although it might seem logical to automatically monitor the target of a symbolic link, consider that if the target is on an automounted filesystem, monitoring the target triggers and holds an automount. Another reason to monitor the link instead of the target is that the target may not exist.

There is no general solution for monitoring targets of symbolic links. You might decide that it's appropriate for your application to monitor a target even if it's automounted.


Tip: The libc routine realpath(3C) is useful when you need to resolve a link into its ultimate target.



Tip: Use statvfs(2) to recognize a remote file.

On the other hand, to avoid triggering and holding an automount, you can manually follow symbolic links until you reach either a local target, which you can then monitor, or a non-existent filesystem, in which case you might decide not to monitor the target. Another option is to test the target once to see if it is local, which triggers an automount only once if the target is automounted.

For example, the following routine determines if a given path is nonexistent, a dangling link, local, or remote.

#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/statvfs.h>

/*
 *  determine a file's location
 */

enum location { ERROR, NONEXISTENT, DANGLING, LOCAL, REMOTE };

enum location
file_location(const char *path)
{
     char target_path[PATH_MAX];
     struct stat statbuf;
     struct statvfs svfsbuf;

     if (!realpath(path, target_path))
     {
           /*
            *  realpath failed -- probably a permission
            *  problem, dangling link or nonexistent file.
            */
            if (errno == EACCES)
                return ERROR;
            if (lstat(path, &statbuf) == 0)
                return DANGLING;
            else if (errno == ENOENT)
                return NONEXISTENT;
            else
                return ERROR;
      }
     /*
      *  Realpath succeeded.  Find out if file is local.
      */
      if (statvfs(target_path, &svfsbuf) < 0)
          return ERROR;
      if (svfsbuf.f_flag & ST_LOCAL)
          return LOCAL;
      else
          return REMOTE;
}

FAM Examples

The following examples show event streams that FAM sends in certain situations.

Example: A client monitors an existing file. Later, another program appends data to the file. Even later, the client cancels the monitoring request.

User calls     FAMMonitorFile(... “/a/b/c” ...)
FAM events:    FAMExists       /a/b/c
               FAMEndExist     /a/b/c

Other program appends to file.
FAM event:     FAMChanged      /a/b/c

User calls     FAMCancelMonitor(...)
FAM event:     FAMAcknowledge  /a/b/c

Example: A client monitors a directory containing two files. Later, another program creates a third file.

User calls     FAMMonitorDirectory(... “/a/b” ...)
FAM events:    FAMExists       /a/b
               FAMExists       file_one
               FAMExists       file_two
               FAMEndExist     /a/b
Third file created.
               FAMCreated      file_three

Example: A client monitors an executable file which is already running. Later, the program exits.

User calls     FAMMonitorFile(... “/a/b/program” ...)
FAM events:    FAMExists       /a/b/program
               FAMEndExist     /a/b/program
               FAMStartExecuting /a/b/program
Program exits.
FAM event:     FAMStopExecuting /a/b/program

Example: A client makes an invalid request.

User calls     FAMMonitorDirectory(... “relative/path” ...)
FAM event:     FAMAcknowledge  relative/path

Example: A client monitors a nonexistent file. Later, another program creates the file.

User calls     FAMMonitorFile(... “/a/b/c” ...)
FAM events:    FAMDeleted      /a/b/c
               FAMEndExist
File is created.
FAM event:     FAMCreated      /a/b/c

Example: A client monitors a directory containing some files. Another program deletes the directory, then creates a new file with the same name as the directory.

User calls     FAMMonitorDirectory(... “/a/b” ...)
FAM events:    FAMExists       /a/b
               FAMExists       file_one
               FAMExists       file_two
               FAMEndExist     /a/b
Directory and files are deleted.
FAM events:    FAMDeleted      /a/b
               FAMChanged      /a/b
               FAMDeleted      file_one
               FAMDeleted      file_two
File with same name created.
FAM events:    FAMCreated      /a/b
               FAMChanged      /a/b

Using FAM

As noted in “Detecting Changes to Files and Directories”, your application can check for changes in files in directories that it monitors in two ways:

This section describes how to use both approaches.

Waiting for File Changes

Follow these steps to use FAM in your application, using the select approach to detect changes:

  1. Call FAMOpen() to create a connection to fam. This routine returns a FAMConnection structure used in all FAM procedures.

  2. Call FAMMonitorFile() and FAMMonitorDirectory() to tell fam which files and directories to monitor.

  3. Select on the fam socket file descriptor and call FAMNextEvent() when the fam socket is readable.

  4. When the application is finished monitoring a file or directory, call FAMCancelMonitor(). If you want to temporarily suspend monitoring of a file or directory, call FAMSuspendMonitor(). When you're ready to start monitoring again, call FAMResumeMonitor().

  5. When the application no longer needs to monitor files and directories, call FAMClose() to release resources associated with files still being monitored and to close the connection to fam. This step is optional if you simply exit your application.

Example 8-1 demonstrates this process in a simple program.

Example 8-1. Using Select With FAM

/*
 *   monitor.c -- monitor arbitrary file or directory
 *                using fam
 */
 
#include <fam.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/select.h>
 
/* event_name() - return printable name of fam event code */
 
const char *event_name(int code)
{
    static const char *famevent[] = {
        "",
        "FAMChanged",
        "FAMDeleted",
        "FAMStartExecuting",
        "FAMStopExecuting",
        "FAMCreated",
        "FAMMoved",
        "FAMAcknowledge",
        "FAMExists",
        "FAMEndExist"
    };
    static char unknown_event[10];
 
    if (code < FAMChanged || code > FAMEndExist)
    {
        sprintf(unknown_event, "unknown (%d)", code);
        return unknown_event;
    }
    return famevent[code];
}
 
void main(int argc, char *argv[])
{
    int i, nmon, rc, fam_fd;
    FAMConnection fc;
    FAMRequest *frp;
    struct stat status;
    FAMEvent fe;
    fd_set readfds;
 
    /* Allocate storage for requests */
 
    frp = malloc(argc * sizeof *frp);
    if (!frp)
    {
        perror("malloc");
        exit(1);
    }
 
    /* Open fam connection */
 
    if ((FAMOpen(&fc)) < 0) 
    {
        perror("fam");
        exit(1);
    }
 
    /* Request monitoring for each program argument */
 
    for (nmon = 0, i = 1; i < argc; i++)
    {
        if (stat(argv[i], &status) < 0)
        {
            perror(argv[i]);
            status.st_mode = 0;
        }
        if ((status.st_mode & S_IFMT) == S_IFDIR)
            rc = FAMMonitorDirectory(&fc, argv[i], frp + i,
                                     NULL);
        else
            rc = FAMMonitorFile(&fc, argv[i], frp + i, NULL);
        if (rc < 0)
        {
            perror("FAMMonitor failed");
            continue;
        }
        nmon++;
    }
    if (!nmon)
    {
        fprintf(stderr, "Nothing monitored.\n");
        exit(1);
    }
 
    /* Initialize select data structure */
 
    fam_fd = FAMCONNECTION_GETfd(&fc);
    FD_ZERO(&readfds);
    FD_SET(fam_fd, &readfds);
 
    /* Loop forever. */
 
    while(1)
    {
        if (select(fam_fd + 1, &readfds,
                   NULL, NULL, NULL) < 0)
        {
             perror("select failed");
             exit(1);
        }
        if (FD_ISSET(fam_fd, &readfds))
        {
            if (FAMNextEvent(&fc, &fe) < 0)
            {
                perror("FAMNextEvent");
                exit(1);
            }
            printf("%-24s %s\n", fe.filename,
                   event_name(fe.code));
        }
    }
}


Polling for File Changes

Follow these steps to use FAM in your application, using the polling approach to detect changes:

  1. Call FAMOpen() to create a connection to fam. This routine returns a FAMConnection structure used in all FAM procedures.

  2. Call FAMMonitorFile() and FAMMonitorDirectory() to tell fam which files and directories to monitor.

  3. Call FAMPending() to determine when there is a pending FAM event and then call FAMNextEvent() when an event is detected.

  4. When the application is finished monitoring a file or directory, call FAMCancelMonitor(). If you want to temporarily suspend monitoring of a file or directory, call FAMSuspendMonitor(). When you're ready to start monitoring again, call FAMResumeMonitor().

  5. When the application no longer needs to monitor files and directories, call FAMClose() to free resources associated with files still being monitored and to close the connection to fam. This step is optional if you simply exit your application.

For example, you could use the polling approach in the monitor.c program listed in Example 8-1 by deleting the code pertaining to the select data structure and replacing the while loop with the code shown in Example 8-2, which demonstrates this process in a simple program.

Example 8-2. Polling With FAM

while(1)
{
    rc = FAMPending(&fc);
    if (rc == 0)
        break;
    else if (rc == -1)
        perror("FAMPending");
    if (FAMNextEvent(&fc, &fe) < 0)
    {
        perror("FAMNextEvent");
        exit(1);
    }
    printf("%-24s %s\n", fe.filename,
           event_name(fe.code));
}

This is a particularly useful approach if you want to poll for changes from within an Xt work procedure. Example 8-3 shows the skeleton code for such a work procedure.

Example 8-3. Polling FAM Within an Xt Work Procedure

Boolean monitorFiles(XtPointer clientData)
{
    int rc = FAMPending(&fc);
 
    if (rc == 0)
        return(FALSE);
    else if (rc == -1)
        XtAppError(app_context, "FAMPending error");
 
    if (FAMNextEvent(&fc, &fe) < 0)
    {
        XtAppError(app_context, "FAMNextEvent error");
    }

    handleFileChange(fe);
    return(FALSE);
}