SGI
Open Source
Using FAM with Qt and GTK+

Monitoring files without polling on IRIX and Linux

This is a draft of an article written for
SGI's Developers' News publication. I'm
not sure whether it will be published there
or not; either way, this information will
continue to be available from the
fam & imon home page.

FAM, the File Alteration Monitor, provides a daemon and an API which enables applications to be notified when changes are made to the files and directories they're interested in. For normal files and directories, monitoring is accomplished without polling the filesystem.

FAM has been part of IRIX since 1989, and it has been available on Linux since the source code for fam was released under the GPL and LGPL in March of 2000. The purpose of this article is to show how applications can use the features of the popular GUI toolkits Qt and GTK+ to talk to fam on IRIX and Linux.

Before you start

The Monitoring Changes to Files and Directories chapter in the IRIX Interactive Desktop Integration Guide contains a tutorial and other information on using fam. Although you shouldn't need to know anything about fam, Qt, GTK+, or the example application to understand this article, there are other sources of information online which may be useful; see the links conveniently scattered throughout this document.

Also, it's worth mentioning that development versions of KDE support fam transparently through the KDirWatch class. In a KDE application, you're probably better off using KDirWatch than talking to fam directly: you may prefer its API, and it means you can let KDirWatch worry about polling on platforms or systems which don't have fam. Communicating with fam

Using fam is pretty simple. Here are the steps, taken roughly from the fam(3X) man page:

  1. Create a connection to fam by calling FAMOpen. This initializes the FAMConnection struct used in all fam procedures.
  2. Tell fam which files and directories to monitor by calling FAMMonitorFile and FAMMonitorDirectory.
  3. select on the FAMConnection's file descriptor. When it becomes active, call FAMPending to determine whether a complete FAMEvent has been received, and call FAMNextEvent to retrieve the event. The FAMEvent will tell you which of your monitored files were operated on, and what the operation was.

In a GUI application, it's not acceptable to sit and wait for FAM events; you need to be able to respond to other events as they occur. Fortunately, both Qt and GTK+ give you ways to add your FAMConnection to their main event loop, so messages from fam can be handled in the same loop as other user events. Using Qt's QSocketNotifier class

To add your FAMConnection to Qt's main event loop, you'll need to create a QSocketNotifier on the FAMConnection's file descriptor. You won't do the select yourself; you'll let Qt do it, and your QSocketNotifier will emit its activated signal when there's an event to read.

First, though, you need to set the FAMConnection's file descriptor for non-blocking reads.[1]

     //  Connect to fam (say FAMConnection *fam is a member of our class)
     fam = new FAMConnection;
     if (FAMOpen2(fam, "yourProgram") != 0)
     {
         //  bummer.  bail.
         delete fam;
         fam = NULL;
         ...
         return;
     }
     //  Set the connection to be non-blocking
     int famfd = FAMCONNECTION_GETFD(fam);
     int flags = fcntl(famfd, F_GETFL);
     fcntl(famfd, F_SETFL, flags | O_NONBLOCK);
 

Then create the QSocketNotifier which will register your FAMConnection's file descriptor in Qt's main loop:

     QSocketNotifier *sn = new QSocketNotifier(famfd, QSocketNotifier::Read, 0);
     connect(sn, SIGNAL(activated(int)), SLOT(readFam()));
 

That's all there is to it! When the FAMConnection is ready for reading, the QSocketNotifier will emit its activated() signal, which you've connected to your readFam() slot.

In your readFam() slot (which you can call whatever you want), you'll process FAMEvents. Suppose FAMConnection *fam and QSocketNotifier *famNotifier are member variables of YourClass:

     void
     YourClass::readFam()
     {
         //  We want to read as many events as are available.
         while (FAMPending(fam))
         {
             FAMEvent ev;
             if (FAMNextEvent(fam, &ev) != 1)
             {
                 //  Nuts!  An error!  Shut down & clean up.
                 delete famNotifier;
                 famNotifier = NULL;
                 FAMClose(fam);
                 delete fam;
                 fam = NULL;
                 ...
                 return;
             }
 
             //  We have a FAMEvent, so process it.
             ...
         }
     }
 

Now, according to the man page, when there's an event ready on the FAMConnection, you should call FAMNextEvent instead of FAMPending, and only use FAMPending when you're "polling" the FAMConnection. I recommend against this. According to the man page, FAMNextEvent blocks until a complete event has been received. However, it's conceivable that many FAMEvents might arrive all at once, and that one might fall across the boundary of some buffer, and that only half of the event would be ready for reading; in this case, because our socket has O_NONBLOCK set, FAMNextEvent's attempt to read the rest of the event would fail (with errno set to EAGAIN), even though subsequent calls might succeed. To avoid this slightly weird (and probably unlikely) case, I recommend always calling FAMPending before FAMNextEvent, and only calling FAMNextEvent when FAMPending says there's something to do. [2] Using GTK+ and GDK's gdk_input_add()

The idea here is the same as with Qt's QSocketNotifier. To add your FAMConnection to GTK+'s main event loop, you'll pass your FAMConnection's file descriptor to gdk_input_add:

     /*  Connect to fam  */
     FAMConnection *fam = g_malloc0(sizeof(FAMConnection));
     if (FAMOpen2(fam, "yourProgram") != 0)
     {
         /*  bummer.  bail.  */
         g_free(fam);
         fam = NULL;
         ...
         return;
     }
     /*  Here we're passing the fam connection to readFam, but
     **  we could just as easily use a global FAMConnection instead,
     **  and pass no pointer.
     */
     gdk_input_add(FAMCONNECTION_GETFD(fam), GDK_INPUT_READ, readFam, fam);
 

Your readFam function might look like this:

     static void
     readFam(gpointer data, gint fd, GdkInputCondition condition)
     {
         FAMConnection *fam = (FAMConnection *)data;
 
         /*  We want to read as many events as are available.  */
         while (FAMPending(fam))
         {
             FAMEvent ev;
             if (FAMNextEvent(fam, &ev) != 1)
             {
                 /*  Nuts!  An error!  Shut down & clean up.  */
                 gdk_input_remove(fd);
                 FAMClose(fam);
                 g_free(fam);
                 ...
                 return;
             }
 
             /*  We have a FAMEvent, so process it. */
             ...
         }
     }
 

Note the use of gdk_input_remove to remove your FAMConnection's socket from the main event loop; that's important! (The Qt equivalent is to delete the QSocketNotifier.)

One thing to be careful about is the scope of your FAMConnection. Either use a global or static FAMConnection, or allocate the FAMConnection yourself (as in the previous example). You do not want to do this:

     {
         FAMConnection fc;
         if (FAMOpen2(&fc, "yourProgram") == 0)
         {
             gdk_input_add(FAMMCONNECTION_GETFD(&fc), GDK_INPUT_READ,
                           readFam, &fc);
         }
         ...
     }
     /*  Bad!  At the end of that block, fc went out of scope,
     **  and the pointer passed to readFam() will be bad!
     */
 
An example: adding fam to kbiff

kbiff[3] is a KDE program which polls your mailbox periodically to determine whether you've got new mail. It's a good example because it's small, it's clean, and monitoring files is what it's all about. (And mailbox on IRIX already uses fam.)

If you use kbiff (or other similar tools), you know that nothing can crush your productivity more than clicking the "Check Mail Now" menu item every half-second to reassure yourself that no mail has arrived without receiving your immediate attention. By making kbiff use fam, you can have the peace of mind which comes from knowing your files are being monitored without the latency of polling, and without the system load associated with setting the polling interval to 0 seconds.

More importantly, you'll be able to apply the same approach to more directly useful applications, such as file browsers, web browsers, system administration tools, programs whose behaviors are affected by configuration files, and so on.

Overview

All of kbiff's monitoring is handled by the KBiffMonitor class, which makes it easy to figure out and change how the monitoring is done. Although kbiff handles a variety of mail protocols, the only one we'll use fam for in this example is Unix-style mailbox files. (Because the kbiff code is neatly organized, though, you can see how to use fam for the other mail protocols which poll local files or directories.)

KBiffMonitor's setMailbox method is called at startup, and when the user changes their mailbox. Depending on the protocol, this is where KBiffMonitor sets up the connection to the POP server, IMAP server, etc. This is where we'll connect to fam if the selected protocol can use it.

kbiff also lets the user start and stop monitoring their mailbox while it's running. We can support this in one of two ways: either by opening and closing the fam connection when the user starts and stop monitoring, or by connecting once in setMailbox if appropriate, and calling FAMResumeMonitor and FAMSuspendMonitor when the user starts and stops monitoring. We'll use the second approach, as it demonstrates the use of two more API methods.

Adding to KBiffMonitor

As there may be several places where we'll want to open and close the connection to fam, we'll add protected openFam and closeFam methods. We'll also add a protected checkFam slot, which will be connected to the QSocketNotifier we create when we open the connection.

For keeping track of our connection and any outstanding request, we'll also add the following private member data:

     QSocketNotifier *famNotifier;
     FAMConnection   *fam;
     FAMRequest       famRequest;
 

In addition to storing information about the connection, the fam member will also be used to indicate whether or not we're connected to fam. (If it's NULL, we're not connected!) We'll initialize it to NULL in the constructor, and add a call to our new method closeFam in the destructor.

The FAMRequest is only necessary because we're going to call FAMSuspendMonitor and FAMResumeMonitor.

openFam() and closeFam()

If we're not already connected, openFam() tries to open a new FAMConnection. If successful, it makes the FAMConnection's socket non-blocking, creates a new QSocketNotifier on the socket, and connects the QSocketNotifier's activated signal to KBiffMonitor's checkFam slot.

     bool KBiffMonitor::openFam()
     {
     TRACEINIT("KBiffMonitor::openFam()");
         if (fam)
             return true;
 
         fam = new FAMConnection;
         if (FAMOpen2(fam, "kbiff") != 0)
         {
     TRACE("bummer, FAMOpen failed!");
             delete fam;
             fam = 0;
             return false;
         }
 
         int famFD = FAMCONNECTION_GETFD(fam);
         int flags = fcntl(famFD, F_GETFL);
         if (!(flags & O_NONBLOCK))
             fcntl(famFD, F_SETFL, flags | O_NONBLOCK);
         famNotifier = new QSocketNotifier(famFD, QSocketNotifier::Read, this);
         connect(famNotifier, SIGNAL(activated(int)), SLOT(checkFam()));
         return true;
     }
 

If we're connected, closeFam() deletes the QSocketNotifier, closes and deletes the FAMConnection, and sets the pointer to NULL so that we know we're not connected.

     void KBiffMonitor::closeFam()
     {
     TRACEINIT("KBiffMonitor::closeFam()");
         if (!fam)
             return;
 
         delete famNotifier;
         famNotifier = 0;
         FAMClose(fam);
         delete fam;
         fam = 0;
     }
 

checkFam()

This is the slot which will be called when the QSocketNotifier on the FAMConnection is activated. It means we should process any pending FAM events.

     void KBiffMonitor::checkFam()
     {
     TRACEINIT("KBiffMonitor::checkFam()");
         //  This indicates that there's data to read from the FAM
         //  connection.
         while (fam && FAMPending(fam))
         {
             FAMEvent ev;
             if (FAMNextEvent(fam, &ev) != 1)
             {
     TRACE("bummer, FAMNextEvent failed!");
                 //  This indicates that there's a problem.  Did fam
                 //  croak?  Close the connection.
                 closeFam();
                 return;
             }
             //  Technically we should process the event here, but the
             //  only thing fam is going to tell us is that "something"
             //  happened with our file...
             emit(signal_checkMail());
         }
     }
 

(Simply emitting signal_checkMail is fine for mail protocols which use a single file, but to properly support protocols which use a directory, the processing should be smarter.)

The rest of the changes

After adding those three methods, the only other changes we need to make are to suspend monitoring in stop, resume monitoring in start, and connect and send our request in setMailbox.

The changes to start and stop each consist of one line.

     void KBiffMonitor::start()
     {
     TRACEINIT("KBiffMonitor::start()");
         started  = true;
         oldTimer = startTimer(poll * 1000);
         emit(signal_checkMail());
         if (fam)
             FAMResumeMonitor(fam, &famRequest);
     }
 
     void KBiffMonitor::stop()
     {
     TRACEINIT("KBiffMonitor::stop()");
         if (oldTimer > 0)
             killTimer(oldTimer);
 
         lastSize   = 0;
         oldTimer   = 0;
         mailState  = UnknownState;
         started    = false;
         lastRead.setTime_t(0);
         lastModified.setTime_t(0);
         uidlList.clear();
         if (fam)
             FAMSuspendMonitor(fam, &famRequest);
     }
 

Because setMailbox starts off by closing any open connections to other servers, we'll do the same with fam:

     void KBiffMonitor::setMailbox(KBiffURL& url)
     {
     TRACEINIT("KBiffMonitor::setMailbox()");
         if (imap)
         {
             delete imap;
             imap = 0;
         }
         if (pop)
         {
             delete pop;
             pop = 0;
         }
         if (nntp)
         {
             delete nntp;
             nntp = 0;
         }
         if (fam)
         {
             closeFam();
         }
 

The only other change is to call openFam and ask fam to monitor the right file or directory. Here's the change for the mbox protocol:

         if (protocol == "mbox")
         {
             disconnect(this);
 
             connect(this, SIGNAL(signal_checkMail()), SLOT(checkMbox()));
             mailbox = url.path();
 
             simpleURL = "mbox:" + mailbox;
             if (openFam())
             {
                 FAMMonitorFile(fam, mailbox, &famRequest, 0);
             }
         }
 

The other protocol-specific blocks are similar, so it would be easy to make the other local file- or directory-based protocols use fam. (For monitoring directories, remember to use FAMMonitorDirectory instead of FAMMonitorFile, and remember that our checkFam should be smarter if we're going to monitor directories.)

That's all there is to it?

This example leaves us with a somewhat crudely hacked kbiff; ideally, we would also want to disable and enable polling in openFam and closeFam, and start should only restart the timer if we're not connected to fam. Also, we'd probably want to make the fam code be conditionally compiled so that kbiff would still work on those unfortunate platforms which don't have fam. Notes

[1] "...You need to set the FAMConnection's file descriptor for non-blocking reads." I think this isn't strictly necessary if you only call FAMNextEvent() when FAMPending() returns true, because FAMPending determines whether an event is ready without blocking. However, the Qt people have done such a nice job on their documentation, it would be a shame to disregard it.

[2] One might think that, where the suggestions in this article disagree with the man page, perhaps the man page ought to be changed. That's a reasonable point of view, but consider that if I spent all my time editing man pages, you wouldn't have this delightful article at your disposal.

[3] The original kbiff source in this example is taken from kdenetwork-1.1.2. If you download that from kde.org, you'll also need kdelibs-1.1.2, kdebase-1.1.2, and qt-1.44. If you want to build that on IRIX, I think you need to set the environment variable CXXFLAGS to -DUSE_NAMESPACES before running the configure script. You can get inst'able images from freeware.sgi.com, but I think they don't include kurllabel.h? so kbiff wouldn't compile for me. Also remember on IRIX to use gmake when building KDE stuff.

One final note: I'd like to thank Kurt Granroth for not getting mad when he finds out I've used his kbiff code as an example without actually asking for his permission.




This might have been written by rusty.