r13027: Support file change notifications from FAM.
authorJames Peach <jpeach@samba.org>
Thu, 19 Jan 2006 00:30:16 +0000 (00:30 +0000)
committerGerald (Jerry) Carter <jerry@samba.org>
Wed, 10 Oct 2007 16:06:11 +0000 (11:06 -0500)
source/Makefile.in
source/configure.in
source/param/loadparm.c
source/smbd/notify.c
source/smbd/notify_fam.c [new file with mode: 0644]

index 58d0bb46a150763aa3e87cacc9803904805d55bb..15d22d8fa599962b7724bb285119d2a085820875 100644 (file)
@@ -347,7 +347,7 @@ PROFILES_OBJ = utils/profiles.o \
 
 OPLOCK_OBJ = smbd/oplock.o smbd/oplock_irix.o smbd/oplock_linux.o
 
-NOTIFY_OBJ = smbd/notify.o smbd/notify_hash.o smbd/notify_kernel.o
+NOTIFY_OBJ = smbd/notify.o smbd/notify_hash.o smbd/notify_kernel.o smbd/notify_fam.o
 
 VFS_AUDIT_OBJ = modules/vfs_audit.o
 VFS_EXTD_AUDIT_OBJ = modules/vfs_extd_audit.o
@@ -877,7 +877,7 @@ bin/smbd@EXEEXT@: $(SMBD_OBJ) @BUILD_POPT@ bin/.dummy
        @echo Linking $@
        @$(CC) $(FLAGS) @PIE_LDFLAGS@ -o $@ $(SMBD_OBJ) $(LDFLAGS) $(LDAP_LIBS) \
                $(KRB5LIBS) $(DYNEXP) $(PRINT_LIBS) $(AUTH_LIBS) \
-               $(ACL_LIBS) $(PASSDB_LIBS) $(LIBS) @POPTLIBS@
+               $(ACL_LIBS) $(PASSDB_LIBS) $(LIBS) @POPTLIBS@ @SMBD_LIBS@
 
 bin/nmbd@EXEEXT@: $(NMBD_OBJ) @BUILD_POPT@ bin/.dummy
        @echo Linking $@
@@ -1008,7 +1008,7 @@ bin/nsstest@EXEEXT@: $(NSSTEST_OBJ) bin/.dummy
 
 bin/vfstest@EXEEXT@: $(VFSTEST_OBJ) @BUILD_POPT@ bin/.dummy
        @echo Linking $@
-       @$(CC) $(FLAGS) @PIE_LDFLAGS@ -o $@ $(VFSTEST_OBJ) $(LDFLAGS) $(TERMLDFLAGS) $(TERMLIBS) $(DYNEXP) $(PRINT_LIBS) $(AUTH_LIBS) $(ACL_LIBS) $(LIBS) @POPTLIBS@ $(KRB5LIBS) $(LDAP_LIBS)
+       @$(CC) $(FLAGS) @PIE_LDFLAGS@ -o $@ $(VFSTEST_OBJ) $(LDFLAGS) $(TERMLDFLAGS) $(TERMLIBS) $(DYNEXP) $(PRINT_LIBS) $(AUTH_LIBS) $(ACL_LIBS) $(LIBS) @POPTLIBS@ $(KRB5LIBS) $(LDAP_LIBS) @SMBD_LIBS@
 
 bin/smbiconv@EXEEXT@: $(SMBICONV_OBJ) @BUILD_POPT@ bin/.dummy
        @echo Linking $@
index 147038a930110fa7a2b5eef15953b6537b045f54..44517ae6eab0e1da228b9326c0020d4f004f4f62 100644 (file)
@@ -2174,6 +2174,33 @@ if test x"$samba_cv_HAVE_KERNEL_CHANGE_NOTIFY" = x"yes"; then
     AC_DEFINE(HAVE_KERNEL_CHANGE_NOTIFY,1,[Whether kernel notifies changes])
 fi
 
+#################################################
+# Check if FAM notifications are available. For FAM info, see
+#      http://oss.sgi.com/projects/fam/
+#      http://savannah.nongnu.org/projects/fam/
+
+AC_CHECK_HEADERS(fam.h, [samba_cv_HAVE_FAM_H=yes], [samba_cv_HAVE_FAM_H=no])
+if test x"$samba_cv_HAVE_FAM_H" = x"yes"; then
+    # On IRIX, libfam requires libC, but other FAM implementations might not
+    # need it.
+    AC_CHECK_LIB(fam, FAMOpen2,
+           [samba_cv_HAVE_LIBFAM=yes; samba_fam_libs="-lfam"],
+           [samba_cv_HAVE_LIBFAM=no])
+
+    if test x"$samba_cv_HAVE_LIBFAM" = x"no" ; then
+       samba_fam_xtra=-lC
+       AC_CHECK_LIB_EXT(fam, samba_fam_xtra, FAMOpen2,
+               [samba_cv_HAVE_LIBFAM=yes; samba_fam_libs="-lfam -lC"],
+               [samba_cv_HAVE_LIBFAM=no])
+       unset samba_fam_xtra
+    fi
+fi
+
+if test x"$samba_cv_HAVE_LIBFAM" = x"yes" ; then
+    AC_DEFINE(HAVE_FAM_CHANGE_NOTIFY, 1,
+           [Whether FAM is file notifications are available])
+fi
+
 AC_CACHE_CHECK([for kernel share modes],samba_cv_HAVE_KERNEL_SHARE_MODES,[
 AC_TRY_RUN([
 #include <sys/types.h>
@@ -2194,8 +2221,6 @@ if test x"$samba_cv_HAVE_KERNEL_SHARE_MODES" = x"yes"; then
 fi
 
 
-
-
 AC_CACHE_CHECK([for IRIX kernel oplock type definitions],samba_cv_HAVE_KERNEL_OPLOCKS_IRIX,[
 AC_TRY_COMPILE([#include <sys/types.h>
 #include <fcntl.h>],
@@ -5231,6 +5256,10 @@ AC_TRY_RUN([#include "${srcdir-.}/tests/summary.c"],
 builddir=`pwd`
 AC_SUBST(builddir)
 
+# Stuff the FAM libraries at the end of the smbd link path (if we have them).
+SMBD_LIBS="$samba_fam_libs"
+AC_SUBST(SMBD_LIBS)
+
 dnl Remove -L/usr/lib/? from LDFLAGS and LIBS
 LIB_REMOVE_USR_LIB(LDFLAGS)
 LIB_REMOVE_USR_LIB(LIBS)
index 526bce9b60ebb70e1df8be7f8e93dccdffbe1a3f..83a5b2fc3cfa8717a30f491ed7cb836831e70369 100644 (file)
@@ -294,6 +294,7 @@ typedef struct
        BOOL bUnixExtensions;
        BOOL bDisableNetbios;
        BOOL bKernelChangeNotify;
+       BOOL bFamChangeNotify;
        BOOL bUseKerberosKeytab;
        BOOL bDeferSharingViolations;
        BOOL bEnablePrivileges;
@@ -992,6 +993,7 @@ static struct parm_struct parm_table[] = {
        {"getwd cache", P_BOOL, P_GLOBAL, &use_getwd_cache, NULL, NULL, FLAG_ADVANCED}, 
        {"keepalive", P_INTEGER, P_GLOBAL, &keepalive, NULL, NULL, FLAG_ADVANCED}, 
        {"kernel change notify", P_BOOL, P_GLOBAL, &Globals.bKernelChangeNotify, NULL, NULL, FLAG_ADVANCED}, 
+       {"fam change notify", P_BOOL, P_GLOBAL, &Globals.bFamChangeNotify, NULL, NULL, FLAG_ADVANCED},
 
        {"lpq cache time", P_INTEGER, P_GLOBAL, &Globals.lpqcachetime, NULL, NULL, FLAG_ADVANCED}, 
        {"max smbd processes", P_INTEGER, P_GLOBAL, &Globals.iMaxSmbdProcesses, NULL, NULL, FLAG_ADVANCED}, 
@@ -1486,6 +1488,7 @@ static void init_globals(void)
        Globals.machine_password_timeout = 60 * 60 * 24 * 7;    /* 7 days default. */
        Globals.change_notify_timeout = 60;     /* 1 minute default. */
        Globals.bKernelChangeNotify = True;     /* On if we have it. */
+       Globals.bFamChangeNotify = True;        /* On if we have it. */
        Globals.lm_announce = 2;        /* = Auto: send only if LM clients found */
        Globals.lm_interval = 60;
        Globals.announce_as = ANNOUNCE_AS_NT_SERVER;
@@ -1868,6 +1871,7 @@ FN_GLOBAL_BOOL(lp_use_spnego, &Globals.bUseSpnego)
 FN_GLOBAL_BOOL(lp_client_use_spnego, &Globals.bClientUseSpnego)
 FN_GLOBAL_BOOL(lp_hostname_lookups, &Globals.bHostnameLookups)
 FN_GLOBAL_BOOL(lp_kernel_change_notify, &Globals.bKernelChangeNotify)
+FN_GLOBAL_BOOL(lp_fam_change_notify, &Globals.bFamChangeNotify)
 FN_GLOBAL_BOOL(lp_use_kerberos_keytab, &Globals.bUseKerberosKeytab)
 FN_GLOBAL_BOOL(lp_defer_sharing_violations, &Globals.bDeferSharingViolations)
 FN_GLOBAL_BOOL(lp_enable_privileges, &Globals.bEnablePrivileges)
index bc76cfb322fb3542d4da07de3861b8a5b9c58094..df3d45d20b5005a0f73ccd4ee1441ddd64204308 100644 (file)
@@ -212,9 +212,15 @@ BOOL change_notify_set(char *inbuf, files_struct *fsp, connection_struct *conn,
 
 BOOL init_change_notify(void)
 {
+       cnotify = NULL;
+
 #if HAVE_KERNEL_CHANGE_NOTIFY
-       if (lp_kernel_change_notify())
+       if (cnotify == NULL && lp_kernel_change_notify())
                cnotify = kernel_notify_init();
+#endif
+#if HAVE_FAM_CHANGE_NOTIFY
+       if (cnotify == NULL && lp_fam_change_notify())
+               cnotify = fam_notify_init();
 #endif
        if (!cnotify) cnotify = hash_notify_init();
        
diff --git a/source/smbd/notify_fam.c b/source/smbd/notify_fam.c
new file mode 100644 (file)
index 0000000..4133402
--- /dev/null
@@ -0,0 +1,446 @@
+/*
+ * FAM file notification support.
+ *
+ * Copyright (c) James Peach 2005
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+ */
+
+#include "includes.h"
+
+#ifdef HAVE_FAM_CHANGE_NOTIFY
+
+#include <fam.h>
+
+/* NOTE: There are multiple versions of FAM floating around the net, each with
+ * slight differences from the original SGI FAM implementation. In this file,
+ * we rely only on the SGI features and do not assume any extensions. For
+ * example, we do not look at FAMErrno, because it is not set by the original
+ * implementation.
+ *
+ * Random FAM links:
+ *     http://oss.sgi.com/projects/fam/
+ *     http://savannah.nongnu.org/projects/fam/
+ *     http://sourceforge.net/projects/bsdfam/
+ */
+
+struct fam_req_info
+{
+    FAMRequest     req;
+    int                    generation;
+    enum FAMCodes   code;
+    enum
+    {
+       /* We are waiting for an event. */
+       FAM_REQ_MONITORING,
+       /* An event has been receive, but we haven't been able to send it back
+        * to the client yet. It is stashed in the code member.
+        */
+       FAM_REQ_FIRED
+    } state;
+};
+
+/* Don't initialise this until the first register request. We want a single
+ * FAM connection for each worker smbd. If we allow the master (parent) smbd to
+ * open a FAM connection, multiple processes talking on the same socket will
+ * undoubtedly create havoc.
+ */
+static FAMConnection   global_fc;
+static int             global_fc_generation;
+
+#define FAM_TRACE      8
+#define FAM_TRACE_LOW  10
+
+#define FAM_NOTIFY_CHECK_TIMEOUT    1 /* secs */
+#define FAM_EVENT_DRAIN                    ((uint32_t)(-1))
+
+/* Turn a FAM event code into a string. Don't rely on specific code values,
+ * because that might not work across all flavours of FAM.
+ */
+static const char *
+fam_event_str(enum FAMCodes code)
+{
+    static struct { enum FAMCodes code; const char * name; } evstr[] =
+    {
+       { FAMChanged,           "FAMChanged"},
+       { FAMDeleted,           "FAMDeleted"},
+       { FAMStartExecuting,    "FAMStartExecuting"},
+       { FAMStopExecuting,     "FAMStopExecuting"},
+       { FAMCreated,           "FAMCreated"},
+       { FAMMoved,             "FAMMoved"},
+       { FAMAcknowledge,       "FAMAcknowledge"},
+       { FAMExists,            "FAMExists"},
+       { FAMEndExist,          "FAMEndExist"}
+    };
+
+    int i;
+
+    for (i = 0; i < ARRAY_SIZE(evstr); ++i) {
+       if (code == evstr[i].code)
+           return(evstr[i].name);
+    }
+
+    return("<unknown>");
+}
+
+static BOOL
+fam_check_reconnect(void)
+{
+    if (FAMCONNECTION_GETFD(&global_fc) < 0) {
+       fstring name;
+
+       global_fc_generation++;
+       snprintf(name, sizeof(name), "smbd (%lu)", (unsigned long)sys_getpid());
+
+       if (FAMOpen2(&global_fc, name) < 0) {
+           DEBUG(0, ("failed to connect to FAM service\n"));
+           return(False);
+       }
+    }
+
+    return(True);
+}
+
+static BOOL
+fam_monitor_path(connection_struct *   conn,
+                struct fam_req_info *  info,
+                const char *           path,
+                uint32                 flags)
+{
+    SMB_STRUCT_STAT st;
+    pstring        fullpath;
+
+    DEBUG(FAM_TRACE, ("requesting FAM notifications for '%s'\n", path));
+
+    /* FAM needs an absolute pathname. */
+
+    /* It would be better to use reduce_name() here, but reduce_name does not
+     * actually return the reduced result. How utterly un-useful.
+     */
+    pstrcpy(fullpath, path);
+    if (!canonicalize_path(conn, fullpath)) {
+       DEBUG(0, ("failed to canonicalize path '%s'\n", path));
+       return(False);
+    }
+
+    if (*fullpath != '/') {
+       DEBUG(0, ("canonicalized path '%s' into `%s`\n", path, fullpath));
+       DEBUGADD(0, ("but expected an absolute path\n"));
+       return(False);
+    }
+
+    if (SMB_VFS_STAT(conn, path, &st) < 0) {
+       DEBUG(0, ("stat of '%s' failed: %s\n", path, strerror(errno)));
+       return(False);
+    }
+    /* Start monitoring this file or directory. We hand the state structure to
+     * both the caller and the FAM library so we can match up the caller's
+     * status requests with FAM notifications.
+     */
+    if (S_ISDIR(st.st_mode)) {
+       FAMMonitorDirectory(&global_fc, fullpath, &(info->req), info);
+    } else {
+       FAMMonitorFile(&global_fc, fullpath, &(info->req), info);
+    }
+
+    /* Grr. On IRIX, neither of the monitor functions return a status. */
+
+    /* We will stay in initialising state until we see the FAMendExist message
+     * for this file.
+     */
+    info->state = FAM_REQ_MONITORING;
+    info->generation = global_fc_generation;
+    return(True);
+}
+
+static BOOL
+fam_handle_event(enum FAMCodes code, uint32 flags)
+{
+#define F_CHANGE_MASK (FILE_NOTIFY_CHANGE_FILE | \
+                       FILE_NOTIFY_CHANGE_ATTRIBUTES | \
+                       FILE_NOTIFY_CHANGE_SIZE | \
+                       FILE_NOTIFY_CHANGE_LAST_WRITE | \
+                       FILE_NOTIFY_CHANGE_LAST_ACCESS | \
+                       FILE_NOTIFY_CHANGE_CREATION | \
+                       FILE_NOTIFY_CHANGE_EA | \
+                       FILE_NOTIFY_CHANGE_SECURITY)
+
+#define F_DELETE_MASK (FILE_NOTIFY_CHANGE_FILE_NAME | \
+                       FILE_NOTIFY_CHANGE_DIR_NAME)
+
+#define F_CREATE_MASK (FILE_NOTIFY_CHANGE_FILE_NAME | \
+                       FILE_NOTIFY_CHANGE_DIR_NAME)
+
+    switch (code) {
+       case FAMChanged:
+           if (flags & F_CHANGE_MASK)
+               return(True);
+           break;
+       case FAMDeleted:
+           if (flags & F_DELETE_MASK)
+               return(True);
+           break;
+       case FAMCreated:
+           if (flags & F_CREATE_MASK)
+               return(True);
+           break;
+       default:
+           /* Ignore anything else. */
+           break;
+    }
+
+    return(False);
+
+#undef F_CHANGE_MASK
+#undef F_DELETE_MASK
+#undef F_CREATE_MASK
+}
+
+static BOOL
+fam_pump_events(struct fam_req_info * info, uint32_t flags)
+{
+    FAMEvent ev;
+
+    for (;;) {
+
+       /* If we are draining the event queue we must keep going until we find
+        * the correct FAMAcknowledge event or the connection drops. Otherwise
+        * we should stop when there are no more events pending.
+        */
+       if (flags != FAM_EVENT_DRAIN && !FAMPending(&global_fc)) {
+           break;
+       }
+
+       if (FAMNextEvent(&global_fc, &ev) < 0) {
+           DEBUG(0, ("failed to fetch pending FAM event\n"));
+           DEBUGADD(0, ("resetting FAM connection\n"));
+           FAMClose(&global_fc);
+           FAMCONNECTION_GETFD(&global_fc) = -1;
+           return(False);
+       }
+
+       DEBUG(FAM_TRACE_LOW, ("FAM event %s on '%s' for request %d\n",
+                   fam_event_str(ev.code), ev.filename, ev.fr.reqnum));
+
+       switch (ev.code) {
+           case FAMAcknowledge:
+               /* FAM generates an ACK event when we cancel a monitor. We need
+                * this to know when it is safe to free out request state
+                * structure.
+                */
+               if (info->generation == global_fc_generation &&
+                   info->req.reqnum == ev.fr.reqnum &&
+                   flags == FAM_EVENT_DRAIN) {
+                   return(True);
+               }
+
+           case FAMEndExist:
+           case FAMExists:
+               /* Ignore these. FAM sends these enumeration events when we
+                * start monitoring. If we are monitoring a directory, we will
+                * get a FAMExists event for each directory entry.
+                */
+
+               /* TODO: we might be able to use these to implement recursive
+                * monitoring of entire subtrees.
+                */
+           case FAMMoved:
+               /* These events never happen. A move or rename shows up as a
+                * create/delete pair.
+                */
+           case FAMStartExecuting:
+           case FAMStopExecuting:
+               /* We might get these, but we just don't care. */
+               break;
+
+           case FAMChanged:
+           case FAMDeleted:
+           case FAMCreated:
+               if (info->generation != global_fc_generation) {
+                   /* Ignore this; the req number can't be matched. */
+                   break;
+               }
+
+               if (info->req.reqnum == ev.fr.reqnum) {
+                   /* This is the event the caller was interested in. */
+                   DEBUG(FAM_TRACE, ("handling FAM %s event on '%s'\n",
+                               fam_event_str(ev.code), ev.filename));
+                   /* Ignore events if we are draining this request. */
+                   if (flags != FAM_EVENT_DRAIN) {
+                       return(fam_handle_event(ev.code, flags));
+                   }
+                   break;
+               } else {
+                   /* Caller doesn't want this event. Stash the result so we
+                    * can come back to it. Unfortunately, FAM doesn't
+                    * guarantee to give us back evinfo.
+                    */
+                   struct fam_req_info * evinfo =
+                       (struct fam_req_info *)ev.userdata;
+
+                   if (evinfo) {
+                       DEBUG(FAM_TRACE, ("storing FAM %s event for winter\n",
+                                   fam_event_str(ev.code)));
+                       evinfo->state = FAM_REQ_FIRED;
+                       evinfo->code = ev.code;
+                   } else {
+                       DEBUG(2, ("received FAM %s notification for %s, "
+                                 "but userdata was unexpectedly NULL\n",
+                                 fam_event_str(ev.code), ev.filename));
+                   }
+                   break;
+               }
+
+           default:
+               DEBUG(0, ("ignoring unknown FAM event code %d for `%s`\n",
+                           ev.code, ev.filename));
+       }
+    }
+
+    /* No more notifications pending. */
+    return(False);
+}
+
+static BOOL
+fam_test_connection(void)
+{
+    FAMConnection fc;
+
+    /* On IRIX FAMOpen2 leaks 960 bytes in 48 blocks. It's a deliberate leak
+     * in the library and there's nothing we can do about it here.
+     */
+    if (FAMOpen2(&fc, "smbd probe") < 0)
+       return(False);
+
+    FAMClose(&fc);
+    return(True);
+}
+
+/* ------------------------------------------------------------------------- */
+
+static void *
+fam_register_notify(connection_struct * conn,
+                   char *              path,
+                   uint32              flags)
+{
+    struct fam_req_info * info;
+
+    if (!fam_check_reconnect()) {
+       return(False);
+    }
+
+    if ((info = SMB_MALLOC_P(struct fam_req_info)) == NULL) {
+       DEBUG(0, ("malloc of %d bytes failed\n", sizeof(struct fam_req_info)));
+       return(NULL);
+    }
+
+    if (fam_monitor_path(conn, info, path, flags)) {
+       return(info);
+    } else {
+       SAFE_FREE(info);
+       return(NULL);
+    }
+}
+
+static BOOL
+fam_check_notify(connection_struct *   conn,
+                uint16_t               vuid,
+                char *                 path,
+                uint32_t               flags,
+                void *                 data,
+                time_t                 when)
+{
+    struct fam_req_info * info;
+
+    info = (struct fam_req_info *)data;
+    SMB_ASSERT(info != NULL);
+
+    DEBUG(10, ("checking FAM events for `%s`\n", path));
+
+    if (info->state == FAM_REQ_FIRED) {
+       DEBUG(FAM_TRACE, ("handling previously fired FAM %s event\n",
+                   fam_event_str(info->code)));
+       info->state = FAM_REQ_MONITORING;
+       return(fam_handle_event(info->code, flags));
+    }
+
+    if (!fam_check_reconnect()) {
+       return(False);
+    }
+
+    if (info->generation != global_fc_generation) {
+       DEBUG(FAM_TRACE, ("reapplying stale FAM monitor to %s\n", path));
+       fam_monitor_path(conn, info, path, flags);
+       return(False);
+    }
+
+    return(fam_pump_events(info, flags));
+}
+
+static void
+fam_remove_notify(void * data)
+{
+    struct fam_req_info * info;
+
+    if ((info = (struct fam_req_info *)data) == NULL)
+       return;
+
+    /* No need to reconnect. If the FAM connection is gone, there's no need to
+     * cancel and we can safely let FAMCancelMonitor fail. If it we
+     * reconnected, then the generation check will stop us cancelling the wrong
+     * request.
+     */
+
+    if (info->generation == global_fc_generation) {
+       DEBUG(FAM_TRACE, ("removing FAM notification for request %d\n",
+                   info->req.reqnum));
+       FAMCancelMonitor(&global_fc, &(info->req));
+
+       /* Soak up all events until the FAMAcknowledge. We can't free
+        * our request state until we are sure there are no more events in
+        * flight.
+        */
+       fam_pump_events(info, FAM_EVENT_DRAIN);
+    }
+
+    SAFE_FREE(info);
+}
+
+struct cnotify_fns * fam_notify_init(void)
+{
+    static struct cnotify_fns global_fam_notify =
+    {
+       fam_register_notify,
+       fam_check_notify,
+       fam_remove_notify,
+       FAM_NOTIFY_CHECK_TIMEOUT
+    };
+
+    /* TODO: rather than relying on FAM_NOTIFY_CHECK_TIMEOUT, we should have an
+     * API to push the FAM fd into the global server fd set.
+     */
+
+    FAMCONNECTION_GETFD(&global_fc) = -1;
+
+    if (!fam_test_connection()) {
+       DEBUG(0, ("FAM file change notifications not available\n"));
+       return(NULL);
+    }
+
+    DEBUG(FAM_TRACE, ("enabling FAM change notifications\n"));
+    return &global_fam_notify;
+}
+
+#endif /* HAVE_FAM_CHANGE_NOTIFY */