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 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 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.

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.

As another example, consider the case where FAM is monitoring a file. If the user deletes the file, FAM correctly reports that fact. However, if the user then creates a new file with the same name, FAM doesn't detect the new file. This is because the new file doesn't have the same inode (in most cases); imon notifies FAM of the new file by inode, but FAM has no record of that inode and so can't match it to the filename. To prevent this from happening, you should cancel monitoring on a file when FAM detects that it's deleted. If you need to detect the creation of a given file by name, you should 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.

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.

You must include <fam.h> in any source file that uses FAM. You must also include <sys/select.h> to use the socket routines to communicate with FAM.

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)

Additionally, you should set a character string variable named appName to the name of your application before calling FAMOpen().

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.

NFS-Mounted Files and Directories

FAM can monitor files and directories that are NFS-mounted, including automounted files and directories. However, because imon doesn't monitor remote files and directories, FAM monitors NFS-mounted files and directories by polling. The polling interval is determined by the -t argument to the FAM daemon, fam, which is invoked by inetd(1M). The default system configuration is to poll every six seconds, but system administrators can change this value by editing /etc/inet.conf.


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


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.

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.

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.

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 that occurred.

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.

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;
    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 the full pathname of the file or directory that changed.

userdata is the arbitrary data pointer that you provided when you called either FAMMonitorFile() or FAMMonitorDirectory() to monitor this file or directory.

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 fstat(1) changed.

FAMDeleted 

A file or directory being monitored was deleted.


Caution: Whenever your application receives a FAMDeleted event for a file or directory, it should cancel monitoring of that file or directory. Otherwise, FAM can generate spurious events.


FAMStartExecuting 


An monitored, executable file started executing. This event occurs every time the file is run, even if older processes are still running.

FAMStopExecuting 


An 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.


Note: This event is generated only for files created in a directory being monitored.


FAMAcknowledge 


FAM generates a FAMAcknowledge event in response to a call to FAMCancelMonitor().


Note: Currently, FAMNextEvent() might not initialize the filename field in a FAMAcknowledge event. You should use the request number to find the file or directory these events reference.


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: Currently, FAMNextEvent() might not initialize the filename field in a FAMEndExist event. You should use the request number to find the file or directory these events reference.


Using FAM

As noted in “Detecting Changes to Files and Directories”, there are two ways that your application can check for changes in files in directories that it monitors: 1) using select() to wait until the FAM socket is active, indicating a change; or 2) using FAMPending() to periodically poll FAM. 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 active.

  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 the Select Method With FAM to Detect Changes to Files and Directories

/*
 *   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 FAM socket data structures */

    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 FAM socket and replacing the while loop with the code shown in Example 8-2.

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

Example 8-2. Using the Polling Method With FAM to Detect Changes to Files and Directories

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);
}